mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-06 04:17:04 +01:00
Compare commits
1 Commits
yuleng/dis
...
niels9001/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c8b2f38c9 |
144
.editorconfig
144
.editorconfig
@@ -1,144 +0,0 @@
|
||||
# EditorConfig for PowerToys - Global Settings
|
||||
|
||||
root = true
|
||||
|
||||
[*.cs]
|
||||
# 禁用过于严格的StyleCop规则
|
||||
|
||||
# 文件结尾换行符
|
||||
dotnet_diagnostic.SA1518.severity = none
|
||||
|
||||
# 空行相关
|
||||
dotnet_diagnostic.SA1505.severity = none
|
||||
dotnet_diagnostic.SA1507.severity = none
|
||||
dotnet_diagnostic.SA1508.severity = none
|
||||
dotnet_diagnostic.SA1513.severity = none
|
||||
dotnet_diagnostic.SA1515.severity = none
|
||||
dotnet_diagnostic.SA1516.severity = none
|
||||
|
||||
# 命名规则 (对于Windows API结构)
|
||||
dotnet_diagnostic.SA1307.severity = none
|
||||
dotnet_diagnostic.SA1313.severity = none
|
||||
|
||||
# Using指令排序
|
||||
dotnet_diagnostic.SA1211.severity = none
|
||||
|
||||
# 多行初始化器尾随逗号
|
||||
dotnet_diagnostic.SA1413.severity = none
|
||||
|
||||
# 参数格式
|
||||
dotnet_diagnostic.SA1116.severity = none
|
||||
dotnet_diagnostic.SA1117.severity = none
|
||||
dotnet_diagnostic.SA1111.severity = none
|
||||
dotnet_diagnostic.SA1128.severity = none
|
||||
|
||||
# 大括号
|
||||
dotnet_diagnostic.SA1503.severity = none
|
||||
|
||||
# 代码格式
|
||||
dotnet_diagnostic.SA1025.severity = none
|
||||
dotnet_diagnostic.SA1028.severity = none
|
||||
dotnet_diagnostic.SA1108.severity = none
|
||||
dotnet_diagnostic.SA1122.severity = none
|
||||
dotnet_diagnostic.SA1129.severity = none
|
||||
dotnet_diagnostic.SA1137.severity = none
|
||||
dotnet_diagnostic.SA1407.severity = none
|
||||
|
||||
# 文件名匹配
|
||||
dotnet_diagnostic.SA1402.severity = none
|
||||
dotnet_diagnostic.SA1649.severity = none
|
||||
|
||||
# P/Invoke相关警告
|
||||
dotnet_diagnostic.CA1401.severity = none
|
||||
dotnet_diagnostic.CA2101.severity = none
|
||||
|
||||
# 其他代码分析警告
|
||||
dotnet_diagnostic.CA1001.severity = none
|
||||
dotnet_diagnostic.CA1305.severity = none
|
||||
dotnet_diagnostic.CA1805.severity = none
|
||||
dotnet_diagnostic.CA1806.severity = none
|
||||
dotnet_diagnostic.CA1816.severity = none
|
||||
dotnet_diagnostic.CA1825.severity = none
|
||||
|
||||
# 代码样式设置
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = crlf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
csharp_using_directive_placement = outside_namespace:silent
|
||||
csharp_prefer_simple_using_statement = true:suggestion
|
||||
csharp_prefer_braces = true:silent
|
||||
csharp_style_namespace_declarations = block_scoped:silent
|
||||
csharp_style_prefer_method_group_conversion = true:silent
|
||||
csharp_style_prefer_top_level_statements = true:silent
|
||||
csharp_style_prefer_primary_constructors = true:suggestion
|
||||
csharp_prefer_system_threading_lock = true:suggestion
|
||||
csharp_style_prefer_simple_property_accessors = true:suggestion
|
||||
csharp_style_expression_bodied_methods = false:silent
|
||||
csharp_style_expression_bodied_constructors = false:silent
|
||||
csharp_style_expression_bodied_operators = false:silent
|
||||
csharp_style_expression_bodied_properties = when_on_single_line:suggestion
|
||||
csharp_style_expression_bodied_indexers = true:silent
|
||||
csharp_style_expression_bodied_accessors = true:silent
|
||||
csharp_style_expression_bodied_lambdas = true:silent
|
||||
csharp_style_expression_bodied_local_functions = false:silent
|
||||
[*.{cs,vb}]
|
||||
#### Naming styles ####
|
||||
|
||||
# Naming rules
|
||||
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
|
||||
|
||||
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
|
||||
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
|
||||
|
||||
# Symbol specifications
|
||||
|
||||
dotnet_naming_symbols.interface.applicable_kinds = interface
|
||||
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.interface.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
|
||||
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.types.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
|
||||
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.non_field_members.required_modifiers =
|
||||
|
||||
# Naming styles
|
||||
|
||||
dotnet_naming_style.begins_with_i.required_prefix = I
|
||||
dotnet_naming_style.begins_with_i.required_suffix =
|
||||
dotnet_naming_style.begins_with_i.word_separator =
|
||||
dotnet_naming_style.begins_with_i.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||
tab_width = 4
|
||||
indent_size = 4
|
||||
end_of_line = crlf
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
|
||||
dotnet_style_prefer_auto_properties = true:silent
|
||||
dotnet_style_object_initializer = false:suggestion
|
||||
dotnet_style_collection_initializer = true:suggestion
|
||||
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
|
||||
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -58,7 +58,6 @@ body:
|
||||
- Image Resizer
|
||||
- Installer
|
||||
- Keyboard Manager
|
||||
- Light Switch
|
||||
- Mouse Utilities
|
||||
- Mouse Without Borders
|
||||
- New+
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/translation_issue.yml
vendored
1
.github/ISSUE_TEMPLATE/translation_issue.yml
vendored
@@ -38,7 +38,6 @@ body:
|
||||
- Image Resizer
|
||||
- Installer
|
||||
- Keyboard Manager
|
||||
- Light Switch
|
||||
- Mouse Utilities
|
||||
- Mouse Without Borders
|
||||
- New+
|
||||
|
||||
2
.github/actions/spell-check/allow/names.txt
vendored
2
.github/actions/spell-check/allow/names.txt
vendored
@@ -29,6 +29,8 @@ shortcutguide
|
||||
|
||||
# 8LWXpg is user name but user folder causes a flag
|
||||
LWXpg
|
||||
# 0x6f677548 is user name but user folder causes a flag
|
||||
x6f677548
|
||||
Adoumie
|
||||
Advaith
|
||||
alekhyareddy
|
||||
|
||||
4
.github/actions/spell-check/excludes.txt
vendored
4
.github/actions/spell-check/excludes.txt
vendored
@@ -121,10 +121,6 @@
|
||||
^src/modules/MouseWithoutBorders/App/Helper/.*\.resx$
|
||||
^src/modules/MouseWithoutBorders/ModuleInterface/generateSecurityDescriptor\.h$
|
||||
^src/modules/peek/Peek.Common/NativeMethods\.txt$
|
||||
^src/modules/peek/Peek.UITests/TestAssets/4\.qoi$
|
||||
^src/modules/powerrename/PowerRenameUITest/testItems/folder1/testCase2\.txt$
|
||||
^src/modules/powerrename/PowerRenameUITest/testItems/folder2/SpecialCase\.txt$
|
||||
^src/modules/powerrename/PowerRenameUITest/testItems/testCase1\.txt$
|
||||
^src/modules/previewpane/SvgPreviewHandler/SvgHTMLPreviewGenerator\.cs$
|
||||
^src/modules/previewpane/UnitTests-MarkdownPreviewHandler/HelperFiles/MarkdownWithHTMLImageTag\.txt$
|
||||
^src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/.*$
|
||||
|
||||
52
.github/actions/spell-check/expect.txt
vendored
52
.github/actions/spell-check/expect.txt
vendored
@@ -26,6 +26,8 @@ ADMINS
|
||||
adml
|
||||
admx
|
||||
advancedpaste
|
||||
advancedpasteui
|
||||
advancedpasteuishortcut
|
||||
advapi
|
||||
advfirewall
|
||||
AFeature
|
||||
@@ -43,6 +45,7 @@ ALLINPUT
|
||||
Allman
|
||||
Allmodule
|
||||
ALLOWUNDO
|
||||
allpc
|
||||
ALLVIEW
|
||||
ALPHATYPE
|
||||
AModifier
|
||||
@@ -133,6 +136,7 @@ bla
|
||||
BLACKFRAME
|
||||
BLENDFUNCTION
|
||||
Blockquotes
|
||||
blogs
|
||||
Blt
|
||||
BLURBEHIND
|
||||
BLURREGION
|
||||
@@ -369,7 +373,7 @@ devmgmt
|
||||
DEVMODE
|
||||
DEVMODEW
|
||||
devpal
|
||||
dfx
|
||||
DFX
|
||||
DIALOGEX
|
||||
digicert
|
||||
DINORMAL
|
||||
@@ -507,8 +511,8 @@ eyetracker
|
||||
FANCYZONESDRAWLAYOUTTEST
|
||||
FANCYZONESEDITOR
|
||||
FARPROC
|
||||
fdx
|
||||
fesf
|
||||
fff
|
||||
FFFF
|
||||
FILEEXPLORER
|
||||
fileexploreraddons
|
||||
@@ -581,7 +585,6 @@ GETSCREENSAVERRUNNING
|
||||
GETSECKEY
|
||||
GETSTICKYKEYS
|
||||
GETTEXTLENGTH
|
||||
gitmodules
|
||||
GHND
|
||||
GMEM
|
||||
GNumber
|
||||
@@ -669,7 +672,11 @@ Hostx
|
||||
hotfixes
|
||||
hotkeycontrol
|
||||
HOTKEYF
|
||||
hotkeylockmachine
|
||||
hotkeyreconnect
|
||||
hotkeys
|
||||
hotkeyswitch
|
||||
hotkeytoggleeasymouse
|
||||
hotlight
|
||||
hotspot
|
||||
HPAINTBUFFER
|
||||
@@ -728,6 +735,8 @@ IMAGERESIZERCONTEXTMENU
|
||||
IMAGERESIZEREXT
|
||||
imageresizerinput
|
||||
imageresizersettings
|
||||
imagetotext
|
||||
imagetotextshortcut
|
||||
imagingdevices
|
||||
ime
|
||||
imgflip
|
||||
@@ -847,7 +856,6 @@ linkid
|
||||
LINKOVERLAY
|
||||
LINQTo
|
||||
listview
|
||||
LIVEDRAW
|
||||
LIVEZOOM
|
||||
LLKH
|
||||
llkhf
|
||||
@@ -859,6 +867,7 @@ localappdata
|
||||
localpackage
|
||||
LOCALSYSTEM
|
||||
LOCATIONCHANGE
|
||||
LOCKMACHINE
|
||||
LOCKTYPE
|
||||
LOGFONT
|
||||
LOGFONTW
|
||||
@@ -867,6 +876,7 @@ LOGMSG
|
||||
LOGPIXELSX
|
||||
LOGPIXELSY
|
||||
lng
|
||||
LOn
|
||||
lon
|
||||
longdate
|
||||
LONGNAMES
|
||||
@@ -918,10 +928,12 @@ luid
|
||||
LUMA
|
||||
lusrmgr
|
||||
LVal
|
||||
lvm
|
||||
LWA
|
||||
lwin
|
||||
LZero
|
||||
MAGTRANSFORM
|
||||
MAJORMINOR
|
||||
MAKEINTRESOURCE
|
||||
MAKEINTRESOURCEA
|
||||
MAKEINTRESOURCEW
|
||||
@@ -946,6 +958,7 @@ MDL
|
||||
mdtext
|
||||
mdtxt
|
||||
mdwn
|
||||
measuretool
|
||||
meme
|
||||
memicmp
|
||||
MENUITEMINFO
|
||||
@@ -995,6 +1008,7 @@ MOUSEHWHEEL
|
||||
MOUSEINPUT
|
||||
mousejump
|
||||
mousepointer
|
||||
mousepointercrosshairs
|
||||
mouseutils
|
||||
MOVESIZEEND
|
||||
MOVESIZESTART
|
||||
@@ -1039,6 +1053,7 @@ MWBEx
|
||||
MYICON
|
||||
NAMECHANGE
|
||||
namespaceanddescendants
|
||||
Namotion
|
||||
nao
|
||||
NCACTIVATE
|
||||
ncc
|
||||
@@ -1076,6 +1091,7 @@ NEWPLUSSHELLEXTENSIONWIN
|
||||
newrow
|
||||
nicksnettravels
|
||||
NIF
|
||||
NJson
|
||||
NLog
|
||||
NLSTEXT
|
||||
NMAKE
|
||||
@@ -1149,7 +1165,6 @@ ntfs
|
||||
NTSTATUS
|
||||
NTSYSAPI
|
||||
NULLCURSOR
|
||||
nullref
|
||||
nullonfailure
|
||||
numberbox
|
||||
nwc
|
||||
@@ -1203,6 +1218,18 @@ PARENTRELATIVEFORUI
|
||||
PARENTRELATIVEPARSING
|
||||
parray
|
||||
PARTIALCONFIRMATIONDIALOGTITLE
|
||||
pasteashtmlfile
|
||||
pasteashtmlfileshortcut
|
||||
pasteasjson
|
||||
pasteasjsonshortcut
|
||||
pasteasmarkdown
|
||||
pasteasmarkdownshortcut
|
||||
pasteasplaintext
|
||||
pasteasplaintextshortcut
|
||||
pasteaspngfile
|
||||
pasteaspngfileshortcut
|
||||
pasteastxtfile
|
||||
pasteastxtfileshortcut
|
||||
PATCOPY
|
||||
PATHMUSTEXIST
|
||||
PATINVERT
|
||||
@@ -1210,7 +1237,6 @@ PATPAINT
|
||||
pbc
|
||||
pbi
|
||||
PBlob
|
||||
pbrush
|
||||
pcb
|
||||
pcch
|
||||
pcelt
|
||||
@@ -1273,6 +1299,7 @@ Pomodoro
|
||||
Popups
|
||||
POPUPWINDOW
|
||||
POSITIONITEM
|
||||
powerocr
|
||||
POWERRENAMECONTEXTMENU
|
||||
powerrenameinput
|
||||
POWERRENAMETEST
|
||||
@@ -1323,6 +1350,7 @@ PRODUCTVERSION
|
||||
Progman
|
||||
programdata
|
||||
projectname
|
||||
projitems
|
||||
PROPERTYKEY
|
||||
Propset
|
||||
PROPVARIANT
|
||||
@@ -1330,7 +1358,6 @@ PRTL
|
||||
prvpane
|
||||
psapi
|
||||
pscid
|
||||
pscustomobject
|
||||
PSECURITY
|
||||
psfgao
|
||||
psfi
|
||||
@@ -1416,6 +1443,7 @@ Removelnk
|
||||
renamable
|
||||
RENAMEONCOLLISION
|
||||
reparented
|
||||
reparenthotkey
|
||||
reparenting
|
||||
reportfileaccesses
|
||||
requery
|
||||
@@ -1441,6 +1469,7 @@ RIDEV
|
||||
RIGHTSCROLLBAR
|
||||
riid
|
||||
RKey
|
||||
Rns
|
||||
RNumber
|
||||
rop
|
||||
ROUNDSMALL
|
||||
@@ -1664,6 +1693,7 @@ STYLECHANGED
|
||||
STYLECHANGING
|
||||
subkeys
|
||||
sublang
|
||||
Subdomain
|
||||
SUBMODULEUPDATE
|
||||
subresource
|
||||
Superbar
|
||||
@@ -1736,6 +1766,7 @@ THICKFRAME
|
||||
THEMECHANGED
|
||||
THISCOMPONENT
|
||||
throughs
|
||||
thumbnailhotkey
|
||||
TILEDWINDOW
|
||||
TILLSON
|
||||
timedate
|
||||
@@ -1751,7 +1782,9 @@ tlbimp
|
||||
tlc
|
||||
tmain
|
||||
TNP
|
||||
TOGGLEEASYMOUSE
|
||||
Toolhelp
|
||||
toolkitconverters
|
||||
toolwindow
|
||||
TOPDOWNDIB
|
||||
TOUCHEVENTF
|
||||
@@ -1763,9 +1796,11 @@ tracelogging
|
||||
tracerpt
|
||||
trackbar
|
||||
trafficmanager
|
||||
transcodetomp
|
||||
transicc
|
||||
TRAYMOUSEMESSAGE
|
||||
triaging
|
||||
Tru
|
||||
trl
|
||||
trx
|
||||
tsa
|
||||
@@ -1801,6 +1836,7 @@ ULONGLONG
|
||||
ums
|
||||
uncompilable
|
||||
UNCPRIORITY
|
||||
undefining
|
||||
UNDNAME
|
||||
UNICODETEXT
|
||||
unins
|
||||
@@ -1967,7 +2003,6 @@ WMI
|
||||
WMICIM
|
||||
wmimgmt
|
||||
wmp
|
||||
wmsg
|
||||
WMSYSCOMMAND
|
||||
wnd
|
||||
WNDCLASS
|
||||
@@ -1981,7 +2016,6 @@ WORKSPACESEDITOR
|
||||
WORKSPACESLAUNCHER
|
||||
WORKSPACESSNAPSHOTTOOL
|
||||
WORKSPACESWINDOWARRANGER
|
||||
worktree
|
||||
wox
|
||||
wparam
|
||||
wpf
|
||||
|
||||
9
.github/actions/spell-check/patterns.txt
vendored
9
.github/actions/spell-check/patterns.txt
vendored
@@ -1,10 +1,5 @@
|
||||
# See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-patterns
|
||||
|
||||
# marker to ignore all code on line
|
||||
^.*/\* #no-spell-check-line \*/.*$
|
||||
# marker for ignoring a comment to the end of the line
|
||||
// #no-spell-check.*$
|
||||
|
||||
# Gaelic
|
||||
Gàidhlig
|
||||
|
||||
@@ -269,7 +264,3 @@ St&yle
|
||||
# This matches a relative clause where the relative pronoun "that" is omitted.
|
||||
# Example: "Gets or sets the window the TitleBar should configure."
|
||||
\bthe\s+\w+\s+the\b
|
||||
|
||||
# Usernames with numbers
|
||||
# 0x6f677548 is user name but user folder causes a flag
|
||||
\bx6f677548\b
|
||||
|
||||
97
.vscode/settings.json
vendored
97
.vscode/settings.json
vendored
@@ -1,97 +0,0 @@
|
||||
{
|
||||
"cmake.ignoreCMakeListsMissing": true,
|
||||
"files.associations": {
|
||||
"algorithm": "cpp",
|
||||
"array": "cpp",
|
||||
"atomic": "cpp",
|
||||
"bit": "cpp",
|
||||
"cctype": "cpp",
|
||||
"charconv": "cpp",
|
||||
"chrono": "cpp",
|
||||
"cinttypes": "cpp",
|
||||
"clocale": "cpp",
|
||||
"cmath": "cpp",
|
||||
"compare": "cpp",
|
||||
"complex": "cpp",
|
||||
"concepts": "cpp",
|
||||
"condition_variable": "cpp",
|
||||
"coroutine": "cpp",
|
||||
"cstddef": "cpp",
|
||||
"cstdint": "cpp",
|
||||
"cstdio": "cpp",
|
||||
"cstdlib": "cpp",
|
||||
"cstring": "cpp",
|
||||
"ctime": "cpp",
|
||||
"cwchar": "cpp",
|
||||
"cwctype": "cpp",
|
||||
"deque": "cpp",
|
||||
"exception": "cpp",
|
||||
"execution": "cpp",
|
||||
"filesystem": "cpp",
|
||||
"format": "cpp",
|
||||
"forward_list": "cpp",
|
||||
"fstream": "cpp",
|
||||
"functional": "cpp",
|
||||
"future": "cpp",
|
||||
"initializer_list": "cpp",
|
||||
"iomanip": "cpp",
|
||||
"ios": "cpp",
|
||||
"iosfwd": "cpp",
|
||||
"iostream": "cpp",
|
||||
"istream": "cpp",
|
||||
"iterator": "cpp",
|
||||
"limits": "cpp",
|
||||
"list": "cpp",
|
||||
"locale": "cpp",
|
||||
"map": "cpp",
|
||||
"memory": "cpp",
|
||||
"memory_resource": "cpp",
|
||||
"mutex": "cpp",
|
||||
"new": "cpp",
|
||||
"numeric": "cpp",
|
||||
"optional": "cpp",
|
||||
"ostream": "cpp",
|
||||
"queue": "cpp",
|
||||
"random": "cpp",
|
||||
"ratio": "cpp",
|
||||
"regex": "cpp",
|
||||
"set": "cpp",
|
||||
"shared_mutex": "cpp",
|
||||
"source_location": "cpp",
|
||||
"span": "cpp",
|
||||
"sstream": "cpp",
|
||||
"stdexcept": "cpp",
|
||||
"stop_token": "cpp",
|
||||
"streambuf": "cpp",
|
||||
"string": "cpp",
|
||||
"strstream": "cpp",
|
||||
"system_error": "cpp",
|
||||
"thread": "cpp",
|
||||
"tuple": "cpp",
|
||||
"type_traits": "cpp",
|
||||
"typeindex": "cpp",
|
||||
"typeinfo": "cpp",
|
||||
"unordered_map": "cpp",
|
||||
"unordered_set": "cpp",
|
||||
"utility": "cpp",
|
||||
"valarray": "cpp",
|
||||
"variant": "cpp",
|
||||
"vector": "cpp",
|
||||
"xfacet": "cpp",
|
||||
"xhash": "cpp",
|
||||
"xiosbase": "cpp",
|
||||
"xlocale": "cpp",
|
||||
"xlocbuf": "cpp",
|
||||
"xlocinfo": "cpp",
|
||||
"xlocmes": "cpp",
|
||||
"xlocmon": "cpp",
|
||||
"xlocnum": "cpp",
|
||||
"xloctime": "cpp",
|
||||
"xmemory": "cpp",
|
||||
"xstddef": "cpp",
|
||||
"xstring": "cpp",
|
||||
"xtr1common": "cpp",
|
||||
"xtree": "cpp",
|
||||
"xutility": "cpp"
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,6 @@
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<!-- Package System.CodeDom added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Management but the 8.0.1 version wasn't published to nuget. -->
|
||||
<PackageVersion Include="System.CodeDom" Version="9.0.8" />
|
||||
<PackageVersion Include="System.Collections.Immutable" Version="9.0.0" />
|
||||
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
<PackageVersion Include="System.ComponentModel.Composition" Version="9.0.8" />
|
||||
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="9.0.8" />
|
||||
|
||||
@@ -514,8 +514,6 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RegistryPreviewExt", "src\m
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RegistryPreview", "RegistryPreview", "{929C1324-22E8-4412-A9A8-80E85F3985A5}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PowerDisplay", "PowerDisplay", "{B5E6F789-0123-4567-8901-23456789ABCD}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FilePreviewCommon", "src\common\FilePreviewCommon\FilePreviewCommon.csproj", "{9EBAA524-0EDA-470B-95D4-39383285CBB2}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.PowerToys", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.PowerToys\Microsoft.PowerToys.Run.Plugin.PowerToys.csproj", "{500DED3E-CFB5-4ED5-ACC6-02B3D6DC336D}"
|
||||
@@ -562,10 +560,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hosts", "src\modules\Hosts\
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RegistryPreview", "src\modules\registrypreview\RegistryPreview\RegistryPreview.csproj", "{8E23E173-7127-4A5F-9F93-3049F2B68047}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerDisplay", "src\modules\powerdisplay\PowerDisplay\PowerDisplay.csproj", "{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerDisplayExt", "src\modules\powerdisplay\PowerDisplayExt\PowerDisplayExt.vcxproj", "{D1234567-8901-2345-6789-ABCDEF012345}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EnvironmentVariables", "src\modules\EnvironmentVariables\EnvironmentVariables\EnvironmentVariables.csproj", "{DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FancyZonesEditorCommon", "src\modules\fancyzones\FancyZonesEditorCommon\FancyZonesEditorCommon.csproj", "{C0974915-8A1D-4BF0-977B-9587D3807AB7}"
|
||||
@@ -831,8 +825,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightSwitch.UITests", "src\
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests.csproj", "{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.UI.ViewModels.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.UI.ViewModels.UnitTests\Microsoft.CmdPal.UI.ViewModels.UnitTests.csproj", "{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|ARM64 = Debug|ARM64
|
||||
@@ -2241,22 +2233,6 @@ Global
|
||||
{8E23E173-7127-4A5F-9F93-3049F2B68047}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{8E23E173-7127-4A5F-9F93-3049F2B68047}.Release|x64.ActiveCfg = Release|x64
|
||||
{8E23E173-7127-4A5F-9F93-3049F2B68047}.Release|x64.Build.0 = Release|x64
|
||||
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Debug|x64.Build.0 = Debug|x64
|
||||
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Release|x64.ActiveCfg = Release|x64
|
||||
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Release|x64.Build.0 = Release|x64
|
||||
{D1234567-8901-2345-6789-ABCDEF012345}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{D1234567-8901-2345-6789-ABCDEF012345}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{D1234567-8901-2345-6789-ABCDEF012345}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{D1234567-8901-2345-6789-ABCDEF012345}.Debug|x64.Build.0 = Debug|x64
|
||||
{D1234567-8901-2345-6789-ABCDEF012345}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{D1234567-8901-2345-6789-ABCDEF012345}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{D1234567-8901-2345-6789-ABCDEF012345}.Release|x64.ActiveCfg = Release|x64
|
||||
{D1234567-8901-2345-6789-ABCDEF012345}.Release|x64.Build.0 = Release|x64
|
||||
{DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}.Debug|x64.ActiveCfg = Debug|x64
|
||||
@@ -3019,14 +2995,6 @@ Global
|
||||
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.ActiveCfg = Release|x64
|
||||
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.Build.0 = Release|x64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Debug|x64.Build.0 = Debug|x64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|x64.ActiveCfg = Release|x64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -3233,9 +3201,6 @@ Global
|
||||
{C32D254F-7597-4CBE-BF74-D922D81CDF29} = {9873BA05-4C41-4819-9283-CF45D795431B}
|
||||
{02DD46D3-F761-47D9-8894-2D6DA0124650} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA}
|
||||
{8E23E173-7127-4A5F-9F93-3049F2B68047} = {929C1324-22E8-4412-A9A8-80E85F3985A5}
|
||||
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF} = {B5E6F789-0123-4567-8901-23456789ABCD}
|
||||
{D1234567-8901-2345-6789-ABCDEF012345} = {B5E6F789-0123-4567-8901-23456789ABCD}
|
||||
{B5E6F789-0123-4567-8901-23456789ABCD} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
|
||||
{DFF88D16-D36F-40A4-A955-CDCAA76EF7B8} = {538ED0BB-B863-4B20-98CC-BCDF7FA0B68A}
|
||||
{C0974915-8A1D-4BF0-977B-9587D3807AB7} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD}
|
||||
{1D6893CB-BC0C-46A8-A76C-9728706CA51A} = {557C4636-D7E1-4838-A504-7D19B725EE95}
|
||||
@@ -3358,7 +3323,6 @@ Global
|
||||
{3DCCD936-D085-4869-A1DE-CA6A64152C94} = {5B201255-53C8-490B-A34F-01F05D48A477}
|
||||
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F} = {3DCCD936-D085-4869-A1DE-CA6A64152C94}
|
||||
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
|
||||
|
||||
313
README.md
313
README.md
@@ -10,11 +10,11 @@
|
||||
|
||||
<h3 align="center">
|
||||
<a href="#-installation">Installation</a>
|
||||
<span> . </span>
|
||||
<span> · </span>
|
||||
<a href="https://aka.ms/powertoys-docs">Documentation</a>
|
||||
<span> . </span>
|
||||
<span> · </span>
|
||||
<a href="https://aka.ms/powertoys-releaseblog">Blog</a>
|
||||
<span> . </span>
|
||||
<span> · </span>
|
||||
<a href="#-whats-new">Release notes</a>
|
||||
</h3>
|
||||
<br/><br/>
|
||||
@@ -27,12 +27,11 @@ Microsoft PowerToys is a collection of utilities that help you customize Windows
|
||||
| [<img src="doc/images/icons/Color%20Picker.png" alt="Color Picker icon" height="16"> Color Picker](https://aka.ms/PowerToysOverview_ColorPicker) | [<img src="doc/images/icons/Command%20Not%20Found.png" alt="Command Not Found icon" height="16"> Command Not Found](https://aka.ms/PowerToysOverview_CmdNotFound) | [<img src="doc/images/icons/Command Palette.png" alt="Command Palette icon" height="16"> Command Palette](https://aka.ms/PowerToysOverview_CmdPal) |
|
||||
| [<img src="doc/images/icons/Crop%20And%20Lock.png" alt="Crop and Lock icon" height="16"> Crop And Lock](https://aka.ms/PowerToysOverview_CropAndLock) | [<img src="doc/images/icons/Environment%20Manager.png" alt="Environment Variables icon" height="16"> Environment Variables](https://aka.ms/PowerToysOverview_EnvironmentVariables) | [<img src="doc/images/icons/FancyZones.png" alt="FancyZones icon" height="16"> FancyZones](https://aka.ms/PowerToysOverview_FancyZones) |
|
||||
| [<img src="doc/images/icons/File%20Explorer%20Preview.png" alt="File Explorer Add-ons icon" height="16"> File Explorer Add-ons](https://aka.ms/PowerToysOverview_FileExplorerAddOns) | [<img src="doc/images/icons/File%20Locksmith.png" alt="File Locksmith icon" height="16"> File Locksmith](https://aka.ms/PowerToysOverview_FileLocksmith) | [<img src="doc/images/icons/Host%20File%20Editor.png" alt="Hosts File Editor icon" height="16"> Hosts File Editor](https://aka.ms/PowerToysOverview_HostsFileEditor) |
|
||||
| [<img src="doc/images/icons/Image%20Resizer.png" alt="Image Resizer icon" height="16"> Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [<img src="doc/images/icons/Keyboard%20Manager.png" alt="Keyboard Manager icon" height="16"> Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) | [<img src="doc/images/icons/Light Switch.png" alt="Light Switch icon" height="16"> Light Switch](https://aka.ms/PowerToysOverview_LightSwitch) |
|
||||
| [<img src="doc/images/icons/Find My Mouse.png" alt="Mouse Utilities icon" height="16"> Mouse Utilities](https://aka.ms/PowerToysOverview_MouseUtilities) | [<img src="doc/images/icons/MouseWithoutBorders.png" alt="Mouse Without Borders icon" height="16"> Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) | [<img src="doc/images/icons/NewPlus.png" alt="New+ icon" height="16"> New+](https://aka.ms/PowerToysOverview_NewPlus) |
|
||||
| [<img src="doc/images/icons/Peek.png" alt="Peek icon" height="16"> Peek](https://aka.ms/PowerToysOverview_Peek) | [<img src="doc/images/icons/PowerRename.png" alt="PowerRename icon" height="16"> PowerRename](https://aka.ms/PowerToysOverview_PowerRename) | [<img src="doc/images/icons/PowerToys%20Run.png" alt="PowerToys Run icon" height="16"> PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) |
|
||||
| [<img src="doc/images/icons/PowerAccent.png" alt="Quick Accent icon" height="16"> Quick Accent](https://aka.ms/PowerToysOverview_QuickAccent) | [<img src="doc/images/icons/Registry%20Preview.png" alt="Registry Preview icon" height="16"> Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) | [<img src="doc/images/icons/MeasureTool.png" alt="Screen Ruler icon" height="16"> Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) |
|
||||
| [<img src="doc/images/icons/Shortcut%20Guide.png" alt="Shortcut Guide icon" height="16"> Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) | [<img src="doc/images/icons/PowerOCR.png" alt="Text Extractor icon" height="16"> Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) | [<img src="doc/images/icons/Workspaces.png" alt="Workspaces icon" height="16"> Workspaces](https://aka.ms/PowerToysOverview_Workspaces) |
|
||||
| [<img src="doc/images/icons/ZoomIt.png" alt="ZoomIt icon" height="16"> ZoomIt](https://aka.ms/PowerToysOverview_ZoomIt) | | |
|
||||
| [<img src="doc/images/icons/Image%20Resizer.png" alt="Image Resizer icon" height="16"> Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [<img src="doc/images/icons/Keyboard%20Manager.png" alt="Keyboard Manager icon" height="16"> Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) | [<img src="doc/images/icons/Find My Mouse.png" alt="Mouse Utilities icon" height="16"> Mouse Utilities](https://aka.ms/PowerToysOverview_MouseUtilities) |
|
||||
| [<img src="doc/images/icons/MouseWithoutBorders.png" alt="Mouse Without Borders icon" height="16"> Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) | [<img src="doc/images/icons/NewPlus.png" alt="New+ icon" height="16"> New+](https://aka.ms/PowerToysOverview_NewPlus) | [<img src="doc/images/icons/Peek.png" alt="Peek icon" height="16"> Peek](https://aka.ms/PowerToysOverview_Peek) |
|
||||
| [<img src="doc/images/icons/PowerRename.png" alt="PowerRename icon" height="16"> PowerRename](https://aka.ms/PowerToysOverview_PowerRename) | [<img src="doc/images/icons/PowerToys%20Run.png" alt="PowerToys Run icon" height="16"> PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) | [<img src="doc/images/icons/PowerAccent.png" alt="Quick Accent icon" height="16"> Quick Accent](https://aka.ms/PowerToysOverview_QuickAccent) |
|
||||
| [<img src="doc/images/icons/Registry%20Preview.png" alt="Registry Preview icon" height="16"> Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) | [<img src="doc/images/icons/MeasureTool.png" alt="Screen Ruler icon" height="16"> Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) | [<img src="doc/images/icons/Shortcut%20Guide.png" alt="Shortcut Guide icon" height="16"> Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) |
|
||||
| [<img src="doc/images/icons/PowerOCR.png" alt="Text Extractor icon" height="16"> Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) | [<img src="doc/images/icons/Workspaces.png" alt="Workspaces icon" height="16"> Workspaces](https://aka.ms/PowerToysOverview_Workspaces) | [<img src="doc/images/icons/ZoomIt.png" alt="ZoomIt icon" height="16"> ZoomIt](https://aka.ms/PowerToysOverview_ZoomIt) |
|
||||
|
||||
|
||||
## 📋 Installation
|
||||
@@ -54,19 +53,19 @@ Choose one of the installation methods below:
|
||||
Go to the [PowerToys GitHub releases][github-release-link], click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer.
|
||||
|
||||
<!-- 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.96%22
|
||||
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.95%22
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.0/PowerToysUserSetup-0.95.0-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.0/PowerToysUserSetup-0.95.0-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.0/PowerToysSetup-0.95.0-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.0/PowerToysSetup-0.95.0-arm64.exe
|
||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.95%22
|
||||
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.94%22
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysUserSetup-0.94.0-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysUserSetup-0.94.0-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysSetup-0.94.0-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysSetup-0.94.0-arm64.exe
|
||||
|
||||
| Description | Filename |
|
||||
|----------------|----------|
|
||||
| Per user - x64 | [PowerToysUserSetup-0.95.0-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.95.0-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.95.0-x64.exe][ptMachineX64] |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.95.0-arm64.exe][ptMachineArm64] |
|
||||
| Per user - x64 | [PowerToysUserSetup-0.94.0-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.94.0-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.94.0-x64.exe][ptMachineX64] |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.94.0-arm64.exe][ptMachineArm64] |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -106,179 +105,175 @@ There are [community driven install methods](./doc/unofficialInstallMethods.md)
|
||||
</details>
|
||||
|
||||
## ✨ What's new
|
||||
**Version 0.95 (October 2025)**
|
||||
**Version 0.94 (September 2025)**
|
||||
|
||||
For an in-depth look at the latest changes, visit the [Windows Command Line blog](https://aka.ms/powertoys-releaseblog).
|
||||
|
||||
**✨ Highlights**
|
||||
- **NEW:** The **Light Switch** utility in PowerToys allows you to automatically switch between light and dark themes in Windows based on the time of day.
|
||||
- Command Palette delivered major search performance gains (new fuzzy matcher and smarter fallbacks) improving relevance and speed.
|
||||
- Peek can now be activated using just the Spacebar!
|
||||
- Find My Mouse added transparent spotlight with independent backdrop opacity, boosting focus and accessibility.
|
||||
- Settings now lets you delete shortcuts entirely and ignore conflicts.
|
||||
- Mouse Pointer Crosshairs gained orientation options (vertical / horizontal / both) for customizable accessibility. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
- PowerRename fixed enumeration counter skipping ensuring reliable batch renames. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- ZoomIt restored legacy draw and snipping behaviors, and fixed recording issues, improving reliability. Thanks [@chakrik73](https://github.com/chakrik73)!
|
||||
|
||||
- PowerToys Settings added a Settings search with fuzzy matching, suggestions, a results page, and UX polish to make finding options faster.
|
||||
- A comprehensive hotkey conflict detection system was introduced in Settings to surface and help resolve conflicting shortcuts. Note that the default hotkey settings (Win+Ctrl+Shift+T, Win+Ctrl+V, Win+Ctrl+T, Win+Shift+T) may overlap with existing Windows system shortcuts. This is expected. You can resolve the conflict by assigning different hotkeys.
|
||||
- Mouse Utilities added a “Gliding cursor” accessibility feature to Mouse Pointer Crosshairs for single‑button cursor movement and clicking. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
- The installer was upgraded to WiX 5 after WiX 3 reached end-of-life; this move improved installer security, reliability, and community support.
|
||||
- Tons of bug fixes and improvements for Command Palette, including visual updates and new support for filters on ListPages (handy for extension developers).
|
||||
- Hosts Editor now has a “No leading spaces” option so active host entries can start at column 0 even if others are disabled. Thanks [@mohammed-saalim](https://github.com/mohammed-saalim)!
|
||||
- Context menu registration was moved from the installer to runtime to avoid loading disabled modules (runtime registrations).
|
||||
- Quick Accent now supports Maltese, and frequently used accents appear first (and are remembered across sessions). Thanks [@rovercoder](https://github.com/rovercoder)! [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
|
||||
### Always On Top
|
||||
|
||||
- Fixed the border hover cursor so it shows the arrow instead of the wait cursor. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
|
||||
### Command Palette
|
||||
- Applied conditional margin for icon-only tags to tighten layout. Thanks [@samrueby](https://github.com/samrueby)
|
||||
- Improved the reliability of accessing Command Palette settings through PowerToys Settings and executing other x-cmdpal:// protocol commands. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Enabled AOT by default for improved performance while simplifying publish configs.
|
||||
- Replaced service state color dots with play/pause/stop icons for enhanced accessibility. Thanks [@samrueby](https://github.com/samrueby)
|
||||
- Fixed filter dropdown sync and crash by binding SelectedValue and raising UI-thread notifications. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Ensured long links wrap correctly in details view.
|
||||
- Removed animation and enforced minimum width on filter dropdown for clarity. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Restored focus to More button after ESC closes context menu, improving keyboard flow. Thanks [@chatasweetie](https://github.com/chatasweetie)
|
||||
- Marked main and toast windows as tool windows to keep them out of Alt+Tab while preserving style. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Fixed AOT template and theming issues for filter separators. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Introduced grid layouts (small, medium, gallery) for richer page presentation.
|
||||
- Materialized result lists to avoid rescoring overhead.
|
||||
- Disabled problematic selection TextToSuggest behind environment flag.
|
||||
- Major search performance improvements (new fuzzy matcher, smarter fallbacks, fewer exceptions).
|
||||
- Added context menu "Show Details" command when details pane is hidden.
|
||||
- Reduced window flicker by avoiding unnecessary cloaking. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Restored EmptyContent rendering for blank states. Thanks [@DevLGuilherme](https://github.com/DevLGuilherme)
|
||||
- Saved new state even if prior app state file was corrupt (better resilience). Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Migrated settings window to WinUI TitleBar control. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Prevented crash on duplicate keybindings and simplified matching. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Hotkeys now always respect the “Ignore shortcut in fullscreen” setting. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Hid search box on content pages, improving focus and accessibility, and added Home title. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Blocked Ctrl+I from inserting stray tabs in search box.
|
||||
- Logged HRESULT codes in error logs for deeper diagnostics. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Advanced font and emoji icon classification and alignment improvements. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Ensured that fallback command icons are visible on the extension settings page. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Fixed breadcrumb margin misalignment (visual polish). Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Truncated overly long command labels with ellipsis to prevent overflow.
|
||||
- Added a setting to configure the page transition animation.
|
||||
- Collection of small improvements and nits for Run Commands.
|
||||
- Improved bookmarks performance and experience. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Added Ctrl+O shortcut in Clipboard History to open links directly.
|
||||
- Resolved conflict with external software that blocked Command Palette from hiding.
|
||||
- Updated context menu items to reflect name and icon changes, and ensured application icons are displayed correctly. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Added Alt+Home shortcut to return immediately to the Command Palette home page. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Fixed a crash when displaying code blocks in markdown on detail or content pages. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Fixed an issue where the search bar icon and title were not updated when rapidly switching pages. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Improved the appearance of the search box in the context menu.
|
||||
|
||||
- Applied single-click activation only to pointer input; keyboard always activates immediately. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Let context menus open at the cursor by removing window-bound constraints. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Made error messages clearer with timestamps, HRESULTs, and full details for easier diagnosis. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Prevented crashes and improved robustness when updating providers without commands. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Ensured the Settings window reliably comes to the front when opened. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Replaced the Clipboard History icon with a colorful Fluent icon. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Hardened ContentIcon to avoid duplicate parenting and improve diagnostics. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Standardized null checks using C# pattern matching for safer behavior.
|
||||
- Improved accessibility by focusing the activation shortcut dialog and making text reachable. Thanks [@chatasweetie](https://github.com/chatasweetie)!
|
||||
- Moved the extension SDK to a stable Windows SDK and cleaned up message namespaces.
|
||||
- Added path shortcuts: ~ to home, and / or \\ to system root, plus UNC support. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- Fixed a race in cancellation handling to avoid InvalidOperationException. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Aligned separator styling with WinUI 3 for consistent visuals. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added ARM64 PDBs to the Extensions SDK NuGet for better debugging.
|
||||
- Added single-select filters to DynamicListPage and updated Windows Services sample.
|
||||
- Updated main page placeholder text to better describe what can be searched. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Removed explicit WinAppSDK/WebView2 dependencies from toolkit and API. Thanks [@rluengen](https://github.com/rluengen)!
|
||||
- Added a local keyboard hook to handle the GoBack key reliably. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Propagated alias changes safely and resolved conflicts across view models.
|
||||
- Allowed providers to override Dispose with a virtual method.
|
||||
- Fixed memory leaks by cleaning up removed or cancelled list items.
|
||||
- Sorted DateTime extension results by relevance for better usability.
|
||||
- Reduced search text "jiggling" by avoiding redundant change notifications.
|
||||
- Centralized automation notifications in a UIHelper for better accessibility. Thanks [@chatasweetie](https://github.com/chatasweetie)!
|
||||
- Preserved Adaptive Card action types during trimming via DynamicDependency.
|
||||
- Added an acrylic backdrop and refined styling to the context menu. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Prevented disposed pages and Settings windows from handling stale messages. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Made the extension API easier to evolve without breaking clients.
|
||||
- Added "evil" sample pages to help reproduce tricky bugs.
|
||||
- Fixed WinGet trim-safety issues by replacing LINQ with manual iteration.
|
||||
- Cancelled stale list fetches to avoid older results overwriting newer ones in CmdPal.
|
||||
|
||||
### Command Palette Extensions
|
||||
- Replaced localized WebSearch setting keys with stable literals and numeric history count. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Enabled advanced markdown tables and emphasis extensions. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added setting to choose Clipboard History primary action (Paste vs Copy). Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Added actionable empty-state hints for File Search (search PC / open indexing settings). Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Ensured all WinGet extension assets copy reliably to output. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Improved Run command line parsing for paths with spaces; sped up related tests.
|
||||
- Updated WebSearch extension icon set for enhanced clarity and contrast. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added Terminal profile sort order setting including MRU tracking. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added Uninstall Application command (UWP direct, Win32 via Settings). Thanks [@mKpwnz](https://github.com/mKpwnz)!
|
||||
- Deferred WinGet details loading and added timing logs.
|
||||
- Removed LINQ from All Apps extension for performance.
|
||||
- Added standardized key chord system + shortcuts to File Search commands. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added Terminal channel filter & remembered selection option. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Enabled loading local/data/app images in markdown with sizing hints. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added external extension reload via x-cmdpal://reload (configurable). Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Instant WebSearch history updates with in-memory store & events. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added keep-after-paste option and safe delete with confirmation for Clipboard History. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
### Command Palette extensions
|
||||
|
||||
### Environment Variables
|
||||
- Replaced custom window chrome with WinUI TitleBar for cleaner, maintainable Environment Variables UI.
|
||||
|
||||
### File Locksmith
|
||||
- Adopted WinUI TitleBar to simplify window chrome while preserving appearance.
|
||||
|
||||
### Find My Mouse
|
||||
- Added transparent spotlight support with separate backdrop opacity; migrated to Windows App SDK composition APIs.
|
||||
- Improved empty states and ranking logic for multiple extensions. Thanks [@htcfreek](https://github.com/htcfreek)!
|
||||
- Added app icons to the All Apps "Run" context command when available.
|
||||
- Restored missing builtin icons by standardizing extension dependencies.
|
||||
- Unblocked local deployment by adding WinAppSDK to two sample extensions.
|
||||
|
||||
### Hosts File Editor
|
||||
- Migrated to native WinUI TitleBar for cleaner, maintainable window chrome.
|
||||
|
||||
### Light Switch
|
||||
- Introduced as a brand-new PowerToy module.
|
||||
- Automatically switches between light and dark themes.
|
||||
- Supports time-based scheduling or location-based sunrise/sunset switching.
|
||||
- Supports using a keyboard shortcut to force a change.
|
||||
- Supports filtering changes for Apps and/or System Theme.
|
||||
- Added a "No leading spaces" option so active hosts entries can start at column 0 even when others are disabled. Thanks [@mohammed-saalim](https://github.com/mohammed-saalim)!
|
||||
|
||||
### Mouse Pointer Crosshairs
|
||||
- Added Esc key to cancel active gliding cursor sequence. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
- Added orientation option (vertical / horizontal / both) for crosshairs customization. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
### Image Resizer
|
||||
|
||||
- Fixed Image Resizer localization by installing satellite resources under the WinUI 3 apps culture path.
|
||||
|
||||
### Mouse Utilities
|
||||
|
||||
- Introduced "Gliding cursor" to control the pointer and click with a single hotkey for better accessibility. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
|
||||
### Mouse Without Borders
|
||||
- Continued Common class refactor (part 5/7) by extracting clipboard and init/cleanup logic into focused classes. Thanks [@mikeclayton](https://github.com/mikeclayton)!
|
||||
|
||||
- Fix connection failures caused by conflicting MachineId across machines. Thanks [@noraa-junker](https://github.com/noraa-junker) for troubleshooting!
|
||||
- Blocked Easy Mouse from switching machines during fullscreen apps, with an allow-list for exceptions. Thanks [@dot-tb](https://github.com/dot-tb)!
|
||||
|
||||
### Peek
|
||||
- Added the option to activate Peek with just the Spacebar.
|
||||
|
||||
- Added Visual Studio shared project file types to XML preview and fixed bgcode handler registration. Thanks [@rezanid](https://github.com/rezanid)!
|
||||
- Fixes bgcode preview handler registration and events for reliable previews. Thanks [@pedrolamas](https://github.com/pedrolamas)!
|
||||
|
||||
### PowerRename
|
||||
- Fixed enumeration counter skipping when regex replacement equals original filename (counters now advance reliably). Thanks [@daverayment](https://github.com/daverayment)!
|
||||
|
||||
- Changed the Explorer accelerator key to PowErRename to avoid clashing with the New menu. Thanks [@aaron-ni](https://github.com/aaron-ni)!
|
||||
|
||||
### Quick Accent
|
||||
- Expanded Welsh layout with acute, grave, and dieresis variants for vowels (consistent ordering). Thanks [@PesBandi](https://github.com/PesBandi)!
|
||||
|
||||
### Registry Preview
|
||||
- Migrated to native TitleBar and AppWindow APIs for cleaner window chrome.
|
||||
|
||||
### Screen Ruler
|
||||
- Fixed ARM64 crash by aligning cursor position structure to 8-byte boundary.
|
||||
- Remembered character usage across sessions so frequently used accents appear first. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- Added Maltese language support with specific characters and the Euro symbol. Thanks [@rovercoder](https://github.com/rovercoder)!
|
||||
- Reduced GPU usage issues by making the window Topmost only when the picker is visible. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
|
||||
### Settings
|
||||
- Added ability to ignore specific hotkey conflicts to reduce noise.
|
||||
- Stopped creating backup directory during dry-run status checks (cleaner first-run).
|
||||
- Standardized casing and localization for ZoomIt and modules header.
|
||||
- Improved search results page accessibility and conditional module grouping.
|
||||
|
||||
### ZoomIt
|
||||
- Updated resource file to reflect standalone v9.01 and current copyright year. Thanks [@foxmsft](https://github.com/foxmsft)!
|
||||
- Restored legacy draw/snipping behaviors and fixed recording race conditions. Thanks [@chakrik73](https://github.com/chakrik73)!
|
||||
- Added smooth image option for improved zoom quality using GDI+ for static zoom and Magnifier API for live zoom. Thanks [@markrussinovich](https://github.com/markrussinovich)!
|
||||
- Added telemetry to track usage of the new shortcut conflict detection workflow.
|
||||
- Moved the shutdown action from the title bar to a footer menu item with confirmation. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- Implemented comprehensive hotkey conflict detection with a dedicated resolution dialog.
|
||||
- Added branded visuals for Office and Copilot keys in the KeyVisual control.
|
||||
- Introduced Settings search with fuzzy matching and navigation to specific controls.
|
||||
- Corrected Spanish localization so product names like Awake remain in English across Settings and OOBE.
|
||||
- Simplified the Advanced Paste description in Settings for quicker reading and consistent capitalization. Thanks [@OldUser101](https://github.com/OldUser101)!
|
||||
- Localized conflict messages in the conflict window and dialog.
|
||||
|
||||
### Documentation
|
||||
- New Microsoft Learn documentation for the Light Switch module.
|
||||
- New dev docs for the Light Switch module.
|
||||
### Installer
|
||||
|
||||
### Development (Area-Build & Area-Tests)
|
||||
- Allowed debug launches to continue when modules fail to load, speeding developer iteration.
|
||||
- Fixed spell checker dictionary entry (advapi) to eliminate false error.
|
||||
- Added VS Code development guide and launch configs to streamline cross-editor workflows.
|
||||
- Upgraded Windows App SDK and related dependencies to 1.8 for newer platform features.
|
||||
- Rewrote YAML comment to resolve new spell checker forbidden pattern. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Corrected solution structure by returning misplaced Common project, reducing build confusion.
|
||||
- Modernized build scripts with shared helpers and VS environment autodetection for simpler CLI builds.
|
||||
- Standardized build scripts and platform detection to improve reliability and reuse.
|
||||
- Added missing Command Palette version bump to align module release cadence.
|
||||
- Added EXECUTEDEFAULT term to dictionary to prevent regression build failures. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Introduced nightly pre-warm pipeline and configurable MSBuild cache mode to improve CI performance.
|
||||
- Resolved CI forbidden pattern spelling complaint to keep pipelines green.
|
||||
- Added AI contributor instruction set to clarify code area expectations.
|
||||
- Added accessibility IDs to settings and FancyZones toggles, stabilizing UI tests.
|
||||
- Added automatic log collection on UI test failures to speed root cause analysis.
|
||||
- Stabilized Mouse Utils tests by switching to AccessibilityId selectors.
|
||||
- Added Screen Ruler UI test coverage to validate core measurement workflows.
|
||||
- Upgraded the installer to WiX 5 with silent "Files in Use" handling for smoother winget installs.
|
||||
- Switched Win10 context menu modules to runtime registration and added cleanup on uninstall to avoid stale entries.
|
||||
|
||||
## 🛣️ Roadmap
|
||||
We are planning some nice new features and improvements for the next releases – a revamped Keyboard Manager UI, custom endpoint and local model support for Advanced Paste, Command Palette improvements and a brand-new Shortcut Guide experience! Stay tuned for [v0.96][github-next-release-work]!
|
||||
### Documentation
|
||||
|
||||
- Adds docs for building the installer locally and testing winget installs.
|
||||
- Fixed a broken style guide link in developer documentation. Thanks [@denizmaral](https://github.com/denizmaral)!
|
||||
|
||||
### Development
|
||||
|
||||
- Excluded test and coverage DLLs from BinSkim scans to cut false positives and speed up security analysis.
|
||||
- Simplified NOTICE maintenance by removing version numbers and filtering out Microsoft/System packages.
|
||||
- Improved NuGet dependency validation to prevent package downgrades and catch issues during restore.
|
||||
- Updated UTF.Unknown to a modern version to improve compatibility without breaking changes. Thanks [@304NotModified](https://github.com/304NotModified)!
|
||||
- Refreshed package catalog in CI before installing dependencies to prevent Linux workflow failures.
|
||||
- Refactored CmdPal tests with dependency injection and added coverage for queries and settings.
|
||||
- Added unit tests to verify Close on Enter swaps Copy/Save as expected. Thanks [@mohammed-saalim](https://github.com/mohammed-saalim)!
|
||||
- Added accessibility IDs to CmdPal UI for stable UI tests.
|
||||
- Rewrote system command tests with a new test base and cleaner patterns.
|
||||
- Added unit tests for WebSearch and Shell extensions with mockable settings.
|
||||
- Added unit tests and abstractions for Apps and Bookmarks extensions.
|
||||
- Cleans up AI-generated tests; adds meaningful query tests across extensions.
|
||||
- Removed the obsolete debug dialog from Settings for a smoother developer loop.
|
||||
|
||||
## 🛣️ Roadmap
|
||||
|
||||
For [v0.95][github-next-release-work], we'll work on the items below:
|
||||
|
||||
- Continued Command Palette polish
|
||||
- Working on Shortcut Guide v2 (Thanks [@noraa-junker](https://github.com/noraa-junker)!)
|
||||
- Upgrading Keyboard Manager's editor UI
|
||||
- UI tweaking utility with day/night theme switcher
|
||||
- DSC v3 support for top utilities
|
||||
- New UI automation tests
|
||||
- Stability, bug fixes
|
||||
|
||||
## ❤️ PowerToys Community
|
||||
|
||||
## ❤️ PowerToys Community
|
||||
The PowerToys team is extremely grateful to have the [support of an amazing active community][community-link]. The work you do is incredibly important. PowerToys wouldn't be nearly what it is today without your help filing bugs, updating documentation, guiding the design, or writing features. We want to say thank you and take time to recognize your work. Your contributions and feedback improve PowerToys month after month!
|
||||
|
||||
## Contributing
|
||||
This project welcomes contributions of all types. Besides coding features / bug fixes, other ways to assist include spec writing, design, documentation, and finding bugs. We are excited to work with the power user community to build a set of tools for helping you get the most out of Windows. We ask that **before you start work on a feature that you would like to contribute**, please read our [Contributor's Guide](CONTRIBUTING.md). We would be happy to work with you to figure out the best approach, provide guidance and mentorship throughout feature development, and help avoid any wasted or duplicate effort. Most contributions require you to agree to a [Contributor License Agreement (CLA)][oss-CLA] declaring that you grant us the rights to use your contribution and that you have permission to do so. For guidance on developing for PowerToys, please read the [developer docs](./doc/devdocs) for a detailed breakdown. This includes how to setup your computer to compile.
|
||||
## Contributing
|
||||
|
||||
## Code of Conduct
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct][oss-conduct-code].
|
||||
This project welcomes contributions of all types. Besides coding features / bug fixes, other ways to assist include spec writing, design, documentation, and finding bugs. We are excited to work with the power user community to build a set of tools for helping you get the most out of Windows.
|
||||
|
||||
## Privacy Statement
|
||||
The application logs basic diagnostic data (telemetry). For more privacy information and what we collect, see our [PowerToys Data and Privacy documentation](https://aka.ms/powertoys-data-and-privacy-documentation).
|
||||
We ask that **before you start work on a feature that you would like to contribute**, please read our [Contributor's Guide](CONTRIBUTING.md). We would be happy to work with you to figure out the best approach, provide guidance and mentorship throughout feature development, and help avoid any wasted or duplicate effort.
|
||||
|
||||
[oss-CLA]: https://cla.opensource.microsoft.com
|
||||
[oss-conduct-code]: CODE_OF_CONDUCT.md
|
||||
[community-link]: COMMUNITY.md
|
||||
[github-release-link]: https://aka.ms/installPowerToys
|
||||
[microsoft-store-link]: https://aka.ms/getPowertoys
|
||||
[winget-link]: https://github.com/microsoft/winget-cli#installing-the-client
|
||||
Most contributions require you to agree to a [Contributor License Agreement (CLA)][oss-CLA] declaring that you grant us the rights to use your contribution and that you have permission to do so.
|
||||
|
||||
For guidance on developing for PowerToys, please read the [developer docs](./doc/devdocs) for a detailed breakdown. This includes how to setup your computer to compile.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct][oss-conduct-code].
|
||||
|
||||
## Privacy Statement
|
||||
|
||||
The application logs basic diagnostic data (telemetry). For more privacy information and what we collect, see our [PowerToys Data and Privacy documentation](https://aka.ms/powertoys-data-and-privacy-documentation).
|
||||
|
||||
[oss-CLA]: https://cla.opensource.microsoft.com
|
||||
[oss-conduct-code]: CODE_OF_CONDUCT.md
|
||||
[community-link]: COMMUNITY.md
|
||||
[github-release-link]: https://aka.ms/installPowerToys
|
||||
[microsoft-store-link]: https://aka.ms/getPowertoys
|
||||
[winget-link]: https://github.com/microsoft/winget-cli#installing-the-client
|
||||
[roadmap]: https://github.com/microsoft/PowerToys/wiki/Roadmap
|
||||
[privacy-link]: http://go.microsoft.com/fwlink/?LinkId=521839
|
||||
[loc-bug]: https://github.com/microsoft/PowerToys/issues/new?assignees=&labels=&template=translation_issue.md&title=
|
||||
[usingPowerToys-docs-link]: https://aka.ms/powertoys-docs
|
||||
[privacy-link]: http://go.microsoft.com/fwlink/?LinkId=521839
|
||||
[loc-bug]: https://github.com/microsoft/PowerToys/issues/new?assignees=&labels=&template=translation_issue.md&title=
|
||||
[usingPowerToys-docs-link]: https://aka.ms/powertoys-docs
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
|
||||
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension" >
|
||||
|
||||
<?include $(sys.CURRENTDIR)\Common.wxi?>
|
||||
|
||||
<?define PowerDisplayAssetsFiles=?>
|
||||
<?define PowerDisplayAssetsFilesPath=$(var.BinDir)WinUI3Apps\Assets\PowerDisplay?>
|
||||
|
||||
<Fragment>
|
||||
<!-- Power Display -->
|
||||
<DirectoryRef Id="WinUI3AppsAssetsFolder">
|
||||
<Directory Id="PowerDisplayAssetsInstallFolder" Name="PowerDisplay" />
|
||||
</DirectoryRef>
|
||||
<DirectoryRef Id="PowerDisplayAssetsInstallFolder" FileSource="$(var.PowerDisplayAssetsFilesPath)">
|
||||
<!-- Generated by generateFileComponents.ps1 -->
|
||||
<!--PowerDisplayAssetsFiles_Component_Def-->
|
||||
</DirectoryRef>
|
||||
|
||||
<ComponentGroup Id="PowerDisplayComponentGroup">
|
||||
<Component Id="RemovePowerDisplayFolder" Guid="B8F2E3A5-72C1-4A2D-9B3F-8E5D7C6A4F9B" Directory="PowerDisplayAssetsInstallFolder" >
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="RemovePowerDisplayFolder" Value="" KeyPath="yes"/>
|
||||
</RegistryKey>
|
||||
<RemoveFolder Id="RemoveFolderPowerDisplayAssetsFolder" Directory="PowerDisplayAssetsInstallFolder" On="uninstall"/>
|
||||
</Component>
|
||||
</ComponentGroup>
|
||||
|
||||
</Fragment>
|
||||
</Wix>
|
||||
@@ -65,7 +65,6 @@
|
||||
<ComponentGroupRef Id="KeyboardManagerComponentGroup" />
|
||||
<ComponentGroupRef Id="PeekComponentGroup" />
|
||||
<ComponentGroupRef Id="PowerRenameComponentGroup" />
|
||||
<ComponentGroupRef Id="PowerDisplayComponentGroup" />
|
||||
<ComponentGroupRef Id="RegistryPreviewComponentGroup" />
|
||||
<ComponentGroupRef Id="RunComponentGroup" />
|
||||
<ComponentGroupRef Id="SettingsComponentGroup" />
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
|
||||
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension" >
|
||||
|
||||
<?include $(sys.CURRENTDIR)\Common.wxi?>
|
||||
|
||||
<?define PowerDisplayAssetsFiles=?>
|
||||
<?define PowerDisplayAssetsFilesPath=$(var.BinDir)WinUI3Apps\Assets\PowerDisplay?>
|
||||
|
||||
<Fragment>
|
||||
<!-- Power Display -->
|
||||
<DirectoryRef Id="WinUI3AppsAssetsFolder">
|
||||
<Directory Id="PowerDisplayAssetsInstallFolder" Name="PowerDisplay" />
|
||||
</DirectoryRef>
|
||||
<DirectoryRef Id="PowerDisplayAssetsInstallFolder" FileSource="$(var.PowerDisplayAssetsFilesPath)">
|
||||
<!-- Generated by generateFileComponents.ps1 -->
|
||||
<!--PowerDisplayAssetsFiles_Component_Def-->
|
||||
</DirectoryRef>
|
||||
|
||||
<ComponentGroup Id="PowerDisplayComponentGroup">
|
||||
<Component Id="RemovePowerDisplayFolder" Guid="B8F2E3A5-72C1-4A2D-9B3F-8E5D7C6A4F9B" Directory="PowerDisplayAssetsInstallFolder" >
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="RemovePowerDisplayFolder" Value="" KeyPath="yes"/>
|
||||
</RegistryKey>
|
||||
<RemoveFolder Id="RemoveFolderPowerDisplayAssetsFolder" Directory="PowerDisplayAssetsInstallFolder" On="uninstall"/>
|
||||
</Component>
|
||||
</ComponentGroup>
|
||||
|
||||
</Fragment>
|
||||
</Wix>
|
||||
@@ -53,7 +53,6 @@
|
||||
<ComponentGroupRef Id="LightSwitchComponentGroup" />
|
||||
<ComponentGroupRef Id="PeekComponentGroup" />
|
||||
<ComponentGroupRef Id="PowerRenameComponentGroup" />
|
||||
<ComponentGroupRef Id="PowerDisplayComponentGroup" />
|
||||
<ComponentGroupRef Id="RegistryPreviewComponentGroup" />
|
||||
<ComponentGroupRef Id="RunComponentGroup" />
|
||||
<ComponentGroupRef Id="SettingsComponentGroup" />
|
||||
|
||||
@@ -43,7 +43,6 @@ namespace Common.UI
|
||||
NewPlus,
|
||||
CmdPal,
|
||||
ZoomIt,
|
||||
PowerDisplay,
|
||||
}
|
||||
|
||||
private static string SettingsWindowNameToString(SettingsWindow value)
|
||||
@@ -114,8 +113,6 @@ namespace Common.UI
|
||||
return "CmdPal";
|
||||
case SettingsWindow.ZoomIt:
|
||||
return "ZoomIt";
|
||||
case SettingsWindow.PowerDisplay:
|
||||
return "PowerDisplay";
|
||||
default:
|
||||
{
|
||||
return string.Empty;
|
||||
|
||||
@@ -29,7 +29,6 @@ namespace ManagedCommon
|
||||
PowerRename,
|
||||
PowerLauncher,
|
||||
PowerAccent,
|
||||
PowerDisplay,
|
||||
RegistryPreview,
|
||||
MeasureTool,
|
||||
ShortcutGuide,
|
||||
|
||||
@@ -195,12 +195,4 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
{
|
||||
return CommonSharedConstants::CMDPAL_SHOW_EVENT;
|
||||
}
|
||||
hstring Constants::ShowPowerDisplayEvent()
|
||||
{
|
||||
return CommonSharedConstants::SHOW_POWER_DISPLAY_EVENT;
|
||||
}
|
||||
hstring Constants::TerminatePowerDisplayEvent()
|
||||
{
|
||||
return CommonSharedConstants::TERMINATE_POWER_DISPLAY_EVENT;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,8 +52,6 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
static hstring WorkspacesHotkeyEvent();
|
||||
static hstring PowerToysRunnerTerminateSettingsEvent();
|
||||
static hstring ShowCmdPalEvent();
|
||||
static hstring ShowPowerDisplayEvent();
|
||||
static hstring TerminatePowerDisplayEvent();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -63,14 +63,14 @@
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
|
||||
@@ -131,10 +131,6 @@ namespace CommonSharedConstants
|
||||
const wchar_t ZOOMIT_REFRESH_SETTINGS_EVENT[] = L"Local\\PowerToysZoomIt-RefreshSettingsEvent-f053a563-d519-4b0d-8152-a54489c13324";
|
||||
const wchar_t ZOOMIT_EXIT_EVENT[] = L"Local\\PowerToysZoomIt-ExitEvent-36641ce6-df02-4eac-abea-a3fbf9138220";
|
||||
|
||||
// Path to the events used by PowerDisplay
|
||||
const wchar_t SHOW_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-ShowEvent-d8a4e0e3-2c5b-4a1c-9e7f-8b3d6c1a2f4e";
|
||||
const wchar_t TERMINATE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-TerminateEvent-7b9c2e1f-8a5d-4c3e-9f6b-2a1d8c5e3b7a";
|
||||
|
||||
// used from quick access window
|
||||
const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a";
|
||||
const wchar_t CMDPAL_EXIT_EVENT[] = L"Local\\PowerToysCmdPal-ExitEvent-eb73f6be-3f22-4b36-aee3-62924ba40bfd";
|
||||
|
||||
@@ -1,36 +1,32 @@
|
||||
#include <windows.h>
|
||||
#include "resource.h"
|
||||
#include "../../../common/version/version.h"
|
||||
|
||||
1 VERSIONINFO
|
||||
FILEVERSION FILE_VERSION
|
||||
PRODUCTVERSION PRODUCT_VERSION
|
||||
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||
FILEVERSION 0,1,0,0
|
||||
PRODUCTVERSION 0,1,0,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS VS_FF_DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
#else
|
||||
FILEFLAGS 0x0L
|
||||
FILEFLAGS 0x0L
|
||||
#endif
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
FILETYPE VFT_DLL
|
||||
FILESUBTYPE VFT2_UNKNOWN
|
||||
FILEOS 0x40004L
|
||||
FILETYPE 0x2L
|
||||
FILESUBTYPE 0x0L
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
|
||||
BLOCK "040904b0"
|
||||
BEGIN
|
||||
VALUE "CompanyName", COMPANY_NAME
|
||||
VALUE "FileDescription", FILE_DESCRIPTION
|
||||
VALUE "FileVersion", FILE_VERSION_STRING
|
||||
VALUE "InternalName", INTERNAL_NAME
|
||||
VALUE "LegalCopyright", COPYRIGHT_NOTE
|
||||
VALUE "OriginalFilename", ORIGINAL_FILENAME
|
||||
VALUE "ProductName", PRODUCT_NAME
|
||||
VALUE "ProductVersion", PRODUCT_VERSION_STRING
|
||||
VALUE "CompanyName", "Company Name"
|
||||
VALUE "FileDescription", "Light Switch Module"
|
||||
VALUE "FileVersion", "0.1.0.0"
|
||||
VALUE "InternalName", "Light Switch"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2019 Company Name"
|
||||
VALUE "OriginalFilename", "PowerToys.LightSwitchModuleInterface.dll"
|
||||
VALUE "ProductName", "Light Switch"
|
||||
VALUE "ProductVersion", "0.1.0.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
|
||||
VALUE "Translation", 0x409, 1200
|
||||
END
|
||||
END
|
||||
@@ -108,7 +108,7 @@ public:
|
||||
|
||||
m_force_light_event_handle = CreateDefaultEvent(L"POWERTOYS_LIGHTSWITCH_FORCE_LIGHT");
|
||||
m_force_dark_event_handle = CreateDefaultEvent(L"POWERTOYS_LIGHTSWITCH_FORCE_DARK");
|
||||
m_manual_override_event_handle = CreateEventW(nullptr, TRUE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
|
||||
m_manual_override_event_handle = CreateDefaultEvent(L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
|
||||
|
||||
init_settings();
|
||||
};
|
||||
@@ -460,7 +460,7 @@ public:
|
||||
}
|
||||
else if (hotkeyId == 0)
|
||||
{
|
||||
// get current will return true if in light mode; otherwise false
|
||||
// get current will return true if in light mode, otherwise false
|
||||
Logger::info(L"[Light Switch] Hotkey triggered: Toggle Theme");
|
||||
if (g_settings.m_changeSystem)
|
||||
{
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
//{{NO_DEPENDENCIES}}
|
||||
// Microsoft Visual C++ generated include file.
|
||||
// Used by CalculatorEngineCommon.rc
|
||||
|
||||
//////////////////////////////
|
||||
// Non-localizable
|
||||
|
||||
#define FILE_DESCRIPTION "Light Switch Module"
|
||||
#define INTERNAL_NAME "Light Switch"
|
||||
#define ORIGINAL_FILENAME "PowerToys.LightSwitchModuleInterface.dll"
|
||||
|
||||
// Non-localizable
|
||||
//////////////////////////////
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
#include <string>
|
||||
#include <LightSwitchSettings.h>
|
||||
#include <common/utils/gpo.h>
|
||||
#include <logger/logger_settings.h>
|
||||
#include <logger/logger.h>
|
||||
#include <utils/logger_helper.h>
|
||||
|
||||
SERVICE_STATUS g_ServiceStatus = {};
|
||||
SERVICE_STATUS_HANDLE g_StatusHandle = nullptr;
|
||||
@@ -38,8 +35,6 @@ int _tmain(int argc, TCHAR* argv[])
|
||||
wchar_t serviceName[] = L"LightSwitchService";
|
||||
SERVICE_TABLE_ENTRYW table[] = { { serviceName, ServiceMain }, { nullptr, nullptr } };
|
||||
|
||||
LoggerHelpers::init_logger(L"LightSwitch", L"Service", LogSettings::lightSwitchLoggerName);
|
||||
|
||||
if (!StartServiceCtrlDispatcherW(table))
|
||||
{
|
||||
DWORD err = GetLastError();
|
||||
@@ -111,7 +106,6 @@ VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl)
|
||||
SetServiceStatus(g_StatusHandle, &g_ServiceStatus);
|
||||
|
||||
// Signal the service to stop
|
||||
Logger::info(L"[LightSwitchService] Stop requested, signaling worker thread to exit.");
|
||||
SetEvent(g_ServiceStopEvent);
|
||||
break;
|
||||
|
||||
@@ -132,21 +126,13 @@ static void update_sun_times(auto& settings)
|
||||
|
||||
int newLightTime = newTimes.sunriseHour * 60 + newTimes.sunriseMinute;
|
||||
int newDarkTime = newTimes.sunsetHour * 60 + newTimes.sunsetMinute;
|
||||
try
|
||||
{
|
||||
auto values = PowerToysSettings::PowerToyValues::load_from_settings_file(L"LightSwitch");
|
||||
values.add_property(L"lightTime", newLightTime);
|
||||
values.add_property(L"darkTime", newDarkTime);
|
||||
values.save_to_settings_file();
|
||||
|
||||
Logger::info(L"[LightSwitchService] Updated sun times and saved to config.");
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
std::wstring wmsg(e.what(), e.what() + strlen(e.what()));
|
||||
Logger::error(L"[LightSwitchService] Exception during sun time update: {}", wmsg);
|
||||
}
|
||||
|
||||
auto values = PowerToysSettings::PowerToyValues::load_from_settings_file(L"LightSwitch");
|
||||
values.add_property(L"lightTime", newLightTime);
|
||||
values.add_property(L"darkTime", newDarkTime);
|
||||
values.save_to_settings_file();
|
||||
|
||||
OutputDebugString(L"[LightSwitchService] Updated sun times and saved to config.\n");
|
||||
}
|
||||
|
||||
DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
@@ -156,8 +142,7 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
if (parentPid)
|
||||
hParent = OpenProcess(SYNCHRONIZE, FALSE, parentPid);
|
||||
|
||||
Logger::info(L"[LightSwitchService] Worker thread starting...");
|
||||
Logger::info(L"[LightSwitchService] Parent PID: {}", parentPid);
|
||||
OutputDebugString(L"[LightSwitchService] Worker thread starting...\n");
|
||||
|
||||
// Initialize settings system
|
||||
LightSwitchSettings::instance().InitFileWatcher();
|
||||
@@ -229,19 +214,19 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
update_sun_times(settings);
|
||||
g_lastUpdatedDay = st.wDay;
|
||||
|
||||
Logger::info(L"[LightSwitchService] Recalculated sun times at new day boundary.");
|
||||
OutputDebugString(L"[LightSwitchService] Recalculated sun times at new day boundary.\n");
|
||||
}
|
||||
|
||||
wchar_t msg[160];
|
||||
swprintf_s(msg,
|
||||
L"[LightSwitchService] now=%02d:%02d | light=%02d:%02d | dark=%02d:%02d",
|
||||
L"[LightSwitchService] now=%02d:%02d | light=%02d:%02d | dark=%02d:%02d\n",
|
||||
st.wHour,
|
||||
st.wMinute,
|
||||
settings.lightTime / 60,
|
||||
settings.lightTime % 60,
|
||||
settings.darkTime / 60,
|
||||
settings.darkTime % 60);
|
||||
Logger::info(msg);
|
||||
OutputDebugString(msg);
|
||||
|
||||
// --- Manual override check ---
|
||||
bool manualOverrideActive = false;
|
||||
@@ -257,11 +242,11 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
nowMinutes == (settings.darkTime + settings.sunset_offset) % 1440)
|
||||
{
|
||||
ResetEvent(hManualOverride);
|
||||
Logger::info(L"[LightSwitchService] Manual override cleared at boundary\n");
|
||||
OutputDebugString(L"[LightSwitchService] Manual override cleared at boundary\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Skipping schedule due to manual override\n");
|
||||
OutputDebugString(L"[LightSwitchService] Skipping schedule due to manual override\n");
|
||||
goto sleep_until_next_minute;
|
||||
}
|
||||
}
|
||||
@@ -276,17 +261,10 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
msToNextMinute = 50;
|
||||
|
||||
DWORD wait = WaitForMultipleObjects(count, waits, FALSE, msToNextMinute);
|
||||
if (wait == WAIT_OBJECT_0)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Stop event triggered <20> exiting worker loop.");
|
||||
if (wait == WAIT_OBJECT_0) // stop event
|
||||
break;
|
||||
}
|
||||
if (hParent && wait == WAIT_OBJECT_0 + 1) // parent process exited
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Parent process exited <20> stopping service.");
|
||||
if (hParent && wait == WAIT_OBJECT_0 + 1) // parent exited
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (hManualOverride)
|
||||
@@ -304,8 +282,8 @@ int APIENTRY wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
|
||||
wchar_t msg[160];
|
||||
swprintf_s(
|
||||
msg,
|
||||
L"Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.");
|
||||
Logger::info(msg);
|
||||
L"Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.\n");
|
||||
OutputDebugString(msg);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@@ -28,6 +28,19 @@
|
||||
<ProjectName>LightSwitchService</ProjectName>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
@@ -41,25 +54,84 @@
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\$(MSBuildProjectName)\</OutDir>
|
||||
<TargetName>PowerToys.LightSwitchService</TargetName>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
<PreprocessorDefinitions>%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<AdditionalIncludeDirectories>
|
||||
./../;
|
||||
..\..\..\common;
|
||||
..\..\..\common\logger;
|
||||
..\..\..\common\utils;
|
||||
..\..\..\common\SettingsAPI;
|
||||
..\..\..\common\Telemetry;
|
||||
..\..\..\common;
|
||||
..\..\..\;
|
||||
..\..\..\..\deps\spdlog\include;
|
||||
./;
|
||||
@@ -73,27 +145,8 @@
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="LightSwitchService.cpp" />
|
||||
<ClCompile Include="LightSwitchSettings.cpp" />
|
||||
<ClCompile Include="SettingsConstants.cpp" />
|
||||
<ClCompile Include="ThemeHelper.cpp" />
|
||||
<ClCompile Include="ThemeScheduler.cpp" />
|
||||
<ClCompile Include="WinHookEventIDs.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="LightSwitchService.rc" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="LightSwitchSettings.h" />
|
||||
<ClInclude Include="SettingsConstants.h" />
|
||||
<ClInclude Include="SettingsObserver.h" />
|
||||
<ClInclude Include="ThemeHelper.h" />
|
||||
<ClInclude Include="ThemeScheduler.h" />
|
||||
<ClInclude Include="WinHookEventIDs.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj">
|
||||
<Project>{4aed67b6-55fd-486f-b917-e543dee2cb3c}</Project>
|
||||
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
|
||||
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
|
||||
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
|
||||
@@ -105,10 +158,62 @@
|
||||
<Project>{8f021b46-362b-485c-bfba-ccf83e820cbd}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="LightSwitchService.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="LightSwitchSettings.cpp" />
|
||||
<ClCompile Include="SettingsConstants.cpp" />
|
||||
<ClCompile Include="ThemeHelper.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ThemeScheduler.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="WinHookEventIDs.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="LightSwitchService.rc" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="LightSwitchSettings.h" />
|
||||
<ClInclude Include="SettingsConstants.h" />
|
||||
<ClInclude Include="SettingsObserver.h" />
|
||||
<ClInclude Include="ThemeHelper.h" />
|
||||
<ClInclude Include="ThemeScheduler.h">
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">false</ExcludedFromBuild>
|
||||
</ClInclude>
|
||||
<ClInclude Include="WinHookEventIDs.h" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<Import Project="..\..\..\..\deps\spdlog.props" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
|
||||
</ImportGroup>
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -24,6 +24,15 @@
|
||||
<ClCompile Include="ThemeHelper.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\..\common\SettingsAPI\settings_helpers.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\..\common\SettingsAPI\settings_objects.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\..\common\SettingsAPI\FileWatcher.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="LightSwitchSettings.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
@@ -34,6 +43,9 @@
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="ThemeScheduler.h">
|
||||
<Filter>Header Files</Filter>
|
||||
@@ -57,9 +69,4 @@
|
||||
<ItemGroup>
|
||||
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="LightSwitchService.rc">
|
||||
<Filter>Resource Files</Filter>
|
||||
</ResourceCompile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -150,7 +150,7 @@ namespace LightSwitch.UITests
|
||||
var modeCombobox = testBase.Session.Find<Element>(By.AccessibilityId("ModeSelection_LightSwitch"), 5000);
|
||||
Assert.IsNotNull(modeCombobox, "Mode combobox not found.");
|
||||
|
||||
var neededTabs = 6;
|
||||
var neededTabs = 5;
|
||||
|
||||
if (modeCombobox.Text != "Manual")
|
||||
{
|
||||
@@ -167,7 +167,7 @@ namespace LightSwitch.UITests
|
||||
Assert.IsNotNull(timeline, "Timeline not found.");
|
||||
|
||||
var helpText = timeline.GetAttribute("HelpText");
|
||||
string originalEndValue = GetHelpTextValue(helpText, "End");
|
||||
string originalStartValue = GetHelpTextValue(helpText, "Start");
|
||||
|
||||
for (int i = 0; i < neededTabs; i++)
|
||||
{
|
||||
@@ -179,12 +179,12 @@ namespace LightSwitch.UITests
|
||||
testBase.Session.SendKeys(Key.Enter);
|
||||
|
||||
helpText = timeline.GetAttribute("HelpText");
|
||||
string updatedEndValue = GetHelpTextValue(helpText, "End");
|
||||
string updatedStartValue = GetHelpTextValue(helpText, "Start");
|
||||
|
||||
Assert.AreNotEqual(originalEndValue, updatedEndValue, "Timeline end time should have been updated.");
|
||||
Assert.AreNotEqual(originalStartValue, updatedStartValue, "Timeline start time should have been updated.");
|
||||
|
||||
helpText = timeline.GetAttribute("HelpText");
|
||||
string originalStartValue = GetHelpTextValue(helpText, "Start");
|
||||
string originalEndValue = GetHelpTextValue(helpText, "End");
|
||||
|
||||
testBase.Session.SendKeys(Key.Tab);
|
||||
testBase.Session.SendKeys(Key.Enter);
|
||||
@@ -192,9 +192,9 @@ namespace LightSwitch.UITests
|
||||
testBase.Session.SendKeys(Key.Enter);
|
||||
|
||||
helpText = timeline.GetAttribute("HelpText");
|
||||
string updatedStartValue = GetHelpTextValue(helpText, "Start");
|
||||
string updatedEndValue = GetHelpTextValue(helpText, "End");
|
||||
|
||||
Assert.AreNotEqual(originalStartValue, updatedStartValue, "Timeline start time should have been updated.");
|
||||
Assert.AreNotEqual(originalEndValue, updatedEndValue, "Timeline end time should have been updated.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -259,7 +259,11 @@ namespace LightSwitch.UITests
|
||||
// Click the select city button
|
||||
var setLocationButton = testBase.Session.Find<Element>(By.AccessibilityId("SetLocationButton_LightSwitch"), 5000);
|
||||
Assert.IsNotNull(setLocationButton, "Set location button not found.");
|
||||
setLocationButton.Click(msPostAction: 8000);
|
||||
setLocationButton.Click();
|
||||
|
||||
var syncLocationButton = testBase.Session.Find<Element>(By.AccessibilityId("SyncLocationButton_LightSwitch"), 5000);
|
||||
Assert.IsNotNull(syncLocationButton, "Sync location button not found.");
|
||||
syncLocationButton.Click(msPostAction: 8000);
|
||||
|
||||
var latLong = testBase.Session.Find<Element>(By.AccessibilityId("LocationResultText_LightSwitch"), 5000);
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(latLong.Text));
|
||||
@@ -301,7 +305,6 @@ namespace LightSwitch.UITests
|
||||
string originalStartValue = GetHelpTextValue(helpText, "Start");
|
||||
|
||||
sunriseOffset.Click();
|
||||
testBase.Session.SendKeys(Key.Up);
|
||||
|
||||
helpText = timeline.GetAttribute("HelpText");
|
||||
string updatedStartValue = GetHelpTextValue(helpText, "Start");
|
||||
@@ -316,7 +319,6 @@ namespace LightSwitch.UITests
|
||||
string originalEndValue = GetHelpTextValue(helpText, "End");
|
||||
|
||||
sunsetOffset.Click();
|
||||
testBase.Session.SendKeys(Key.Up);
|
||||
|
||||
helpText = timeline.GetAttribute("HelpText");
|
||||
string updatedEndValue = GetHelpTextValue(helpText, "End");
|
||||
@@ -336,15 +338,9 @@ namespace LightSwitch.UITests
|
||||
var scrollViewer = testBase.Session.Find<Element>(By.AccessibilityId("PageScrollViewer"));
|
||||
systemCheckbox.EnsureVisible(scrollViewer);
|
||||
|
||||
int neededTabs = 10;
|
||||
|
||||
// How do I handle when something is off screen?
|
||||
if (!systemCheckbox.Selected)
|
||||
{
|
||||
for (int i = 0; i < neededTabs; i++)
|
||||
{
|
||||
testBase.Session.SendKeys(Key.Tab);
|
||||
}
|
||||
|
||||
systemCheckbox.Click();
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ enum struct FindMyMouseActivationMethod : int
|
||||
|
||||
constexpr bool FIND_MY_MOUSE_DEFAULT_DO_NOT_ACTIVATE_ON_GAME_MODE = true;
|
||||
// Default colors now include full alpha. Opacity is encoded directly in color alpha (legacy overlay_opacity migrated into A channel)
|
||||
const winrt::Windows::UI::Color FIND_MY_MOUSE_DEFAULT_BACKGROUND_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(128, 0, 0, 0);
|
||||
const winrt::Windows::UI::Color FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(128, 255, 255, 255);
|
||||
const winrt::Windows::UI::Color FIND_MY_MOUSE_DEFAULT_BACKGROUND_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(255, 0, 0, 0);
|
||||
const winrt::Windows::UI::Color FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(255, 255, 255, 255);
|
||||
constexpr int FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_RADIUS = 100;
|
||||
constexpr int FIND_MY_MOUSE_DEFAULT_ANIMATION_DURATION_MS = 500;
|
||||
constexpr int FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_INITIAL_ZOOM = 9;
|
||||
@@ -43,4 +43,4 @@ int FindMyMouseMain(HINSTANCE hinst, const FindMyMouseSettings& settings);
|
||||
void FindMyMouseDisable();
|
||||
bool FindMyMouseIsEnabled();
|
||||
void FindMyMouseApplySettings(const FindMyMouseSettings& settings);
|
||||
HWND GetSonarHwnd() noexcept;
|
||||
HWND GetSonarHwnd() noexcept;
|
||||
@@ -1055,13 +1055,8 @@ namespace MouseWithoutBorders.Class
|
||||
|
||||
if (machineId == 0)
|
||||
{
|
||||
var newMachineId = Common.Ran.Next();
|
||||
_properties.MachineID.Value = newMachineId;
|
||||
machineId = newMachineId;
|
||||
if (!PauseInstantSaving)
|
||||
{
|
||||
SaveSettings();
|
||||
}
|
||||
_properties.MachineID.Value = Common.Ran.Next();
|
||||
machineId = _properties.MachineID.Value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1073,11 +1068,6 @@ namespace MouseWithoutBorders.Class
|
||||
lock (_loadingSettingsLock)
|
||||
{
|
||||
_properties.MachineID.Value = value;
|
||||
machineId = value;
|
||||
if (!PauseInstantSaving)
|
||||
{
|
||||
SaveSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,10 +96,7 @@ typedef struct {
|
||||
#define SHALLOW_DESTROY 2
|
||||
#define LIVE_DRAW_ZOOM 3
|
||||
|
||||
#define PEN_COLOR_HIGHLIGHT(Pencolor) ((Pencolor >> 24) != 0xFF)
|
||||
#define PEN_COLOR_BLUR(Pencolor) ((Pencolor & 0x00FFFFFF) == COLOR_BLUR)
|
||||
|
||||
#define CURSOR_SAVE_MARGIN 4
|
||||
#define PEN_COLOR_HIGHLIGHT(Pencolor) (Pencolor >> 24) != 0xFF
|
||||
|
||||
|
||||
typedef BOOL (__stdcall *type_pGetMonitorInfo)(
|
||||
@@ -146,14 +143,7 @@ typedef BOOL(__stdcall *type_pMagSetWindowFilterList)(
|
||||
int count,
|
||||
HWND* pHWND
|
||||
);
|
||||
typedef BOOL(__stdcall* type_pMagSetLensUseBitmapSmoothing)(
|
||||
_In_ HWND,
|
||||
_In_ BOOL
|
||||
);
|
||||
typedef BOOL(__stdcall* type_MagSetFullscreenUseBitmapSmoothing)(
|
||||
BOOL fUseBitmapSmoothing
|
||||
);
|
||||
typedef BOOL(__stdcall* type_pMagInitialize)(VOID);
|
||||
typedef BOOL (__stdcall *type_pMagInitialize)(VOID);
|
||||
|
||||
typedef BOOL(__stdcall *type_pGetPointerType)(
|
||||
_In_ UINT32 pointerId,
|
||||
|
||||
@@ -121,8 +121,8 @@ FONT 8, "MS Shell Dlg", 0, 0, 0x0
|
||||
BEGIN
|
||||
DEFPUSHBUTTON "OK",IDOK,166,306,50,14
|
||||
PUSHBUTTON "Cancel",IDCANCEL,223,306,50,14
|
||||
LTEXT "ZoomIt v9.10",IDC_VERSION,42,7,73,10
|
||||
LTEXT "Copyright © 2006-2025 Mark Russinovich",IDC_COPYRIGHT,42,17,231,8
|
||||
LTEXT "ZoomIt v9.01",IDC_VERSION,42,7,73,10
|
||||
LTEXT "Copyright © 2006-2025 Mark Russinovich",IDC_COPYRIGHT,42,17,166,8
|
||||
CONTROL "<a HREF=""https://www.sysinternals.com"">Sysinternals - www.sysinternals.com</a>",IDC_LINK,
|
||||
"SysLink",WS_TABSTOP,42,26,150,9
|
||||
ICON "APPICON",IDC_STATIC,12,9,20,20
|
||||
@@ -149,8 +149,7 @@ BEGIN
|
||||
CONTROL "",IDC_TIMER_POS7,"Button",BS_AUTORADIOBUTTON,63,108,10,10
|
||||
CONTROL "",IDC_TIMER_POS8,"Button",BS_AUTORADIOBUTTON,79,108,10,10
|
||||
CONTROL "",IDC_TIMER_POS9,"Button",BS_AUTORADIOBUTTON,97,108,10,10
|
||||
CONTROL "Show background bitmap:",IDC_CHECK_BACKGROUND_FILE,
|
||||
"Button",BS_AUTOCHECKBOX | WS_TABSTOP,3,122,99,10,WS_EX_RIGHT
|
||||
CONTROL "Show background bitmap:",IDC_CHECK_BACKGROUND_FILE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,3,122,99,10,WS_EX_RIGHT
|
||||
CONTROL "Use faded desktop as background",IDC_STATIC_DESKTOP_BACKGROUND,
|
||||
"Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,46,135,125,10
|
||||
CONTROL "Use image file as background",IDC_STATIC_BACKGROUND_FILE,
|
||||
@@ -166,25 +165,23 @@ BEGIN
|
||||
CONTROL "",IDC_STATIC,"Static",SS_BLACKFRAME | SS_SUNKEN,7,196,193,1,WS_EX_CLIENTEDGE
|
||||
END
|
||||
|
||||
ZOOM DIALOGEX 0, 0, 260, 170
|
||||
ZOOM DIALOGEX 0, 0, 260, 158
|
||||
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
|
||||
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||
BEGIN
|
||||
CONTROL "",IDC_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,59,57,80,12
|
||||
LTEXT "After toggling ZoomIt you can zoom in with the mouse wheel or up and down arrow keys. Exit zoom mode with Escape or by pressing the right mouse button.",IDC_STATIC,7,6,246,26
|
||||
LTEXT "Zoom Toggle:",IDC_STATIC,7,59,51,8
|
||||
CONTROL "",IDC_ZOOM_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,53,118,150,15,WS_EX_TRANSPARENT
|
||||
LTEXT "Specify the initial level of magnification when zooming in:",IDC_STATIC,7,105,215,10
|
||||
LTEXT "1.25",IDC_STATIC,52,136,16,8
|
||||
LTEXT "1.5",IDC_STATIC,82,136,12,8
|
||||
LTEXT "1.75",IDC_STATIC,108,136,16,8
|
||||
LTEXT "2.0",IDC_STATIC,138,136,12,8
|
||||
LTEXT "3.0",IDC_STATIC,164,136,12,8
|
||||
LTEXT "4.0",IDC_STATIC,190,136,12,8
|
||||
CONTROL "",IDC_ZOOM_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,53,104,150,15,WS_EX_TRANSPARENT
|
||||
LTEXT "Specify the initial level of magnification when zooming in:",IDC_STATIC,7,91,215,10
|
||||
LTEXT "1.25",IDC_STATIC,52,122,16,8
|
||||
LTEXT "1.5",IDC_STATIC,82,122,12,8
|
||||
LTEXT "1.75",IDC_STATIC,108,122,16,8
|
||||
LTEXT "2.0",IDC_STATIC,138,122,12,8
|
||||
LTEXT "3.0",IDC_STATIC,164,122,12,8
|
||||
LTEXT "4.0",IDC_STATIC,190,122,12,8
|
||||
CONTROL "Animate zoom in and zoom out:",IDC_ANIMATE_ZOOM,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,74,116,10
|
||||
CONTROL "Smooth zoomed image:",IDC_SMOOTH_IMAGE,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,88,116,10
|
||||
LTEXT "Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,7,148,246,17
|
||||
LTEXT "Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,6,34,246,18
|
||||
LTEXT "Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,7,34,246,17
|
||||
END
|
||||
|
||||
DRAW DIALOGEX 0, 0, 260, 228
|
||||
@@ -298,8 +295,7 @@ BEGIN
|
||||
LTEXT "DemoType toggle:",IDC_STATIC,7,157,63,8
|
||||
PUSHBUTTON "&...",IDC_DEMOTYPE_BROWSE,231,137,16,13
|
||||
CONTROL "",IDC_DEMOTYPE_SPEED_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,52,202,150,11,WS_EX_TRANSPARENT
|
||||
CONTROL "Drive input with typing:",IDC_DEMOTYPE_USER_DRIVEN,
|
||||
"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,173,88,10
|
||||
CONTROL "Drive input with typing:",IDC_DEMOTYPE_USER_DRIVEN,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,173,88,10
|
||||
LTEXT "DemoType typing speed:",IDC_STATIC,7,189,215,10
|
||||
LTEXT "Slow",IDC_DEMOTYPE_STATIC1,51,213,18,8
|
||||
LTEXT "Fast",IDC_DEMOTYPE_STATIC2,186,213,17,8
|
||||
@@ -417,8 +413,8 @@ ACCELERATORS ACCELERATORS
|
||||
BEGIN
|
||||
"C", IDC_COPY, VIRTKEY, CONTROL, NOINVERT
|
||||
"S", IDC_SAVE, VIRTKEY, CONTROL, NOINVERT
|
||||
"C", IDC_COPY_CROP, VIRTKEY, SHIFT, CONTROL, NOINVERT
|
||||
"S", IDC_SAVE_CROP, VIRTKEY, SHIFT, CONTROL, NOINVERT
|
||||
"C", IDC_COPY_CROP, VIRTKEY, SHIFT, CONTROL, NOINVERT
|
||||
"S", IDC_SAVE_CROP, VIRTKEY, SHIFT, CONTROL, NOINVERT
|
||||
END
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ DWORD g_SnipToggleKey = ((HOTKEYF_CONTROL) << 8) | '6';
|
||||
DWORD g_ShowExpiredTime = 1;
|
||||
DWORD g_SliderZoomLevel = 3;
|
||||
BOOLEAN g_AnimateZoom = TRUE;
|
||||
BOOLEAN g_SmoothImage = TRUE;
|
||||
DWORD g_PenColor = COLOR_RED;
|
||||
DWORD g_BreakPenColor = COLOR_RED;
|
||||
DWORD g_RootPenWidth = PEN_WIDTH;
|
||||
@@ -73,7 +72,6 @@ REG_SETTING RegSettings[] = {
|
||||
{ L"ShowTrayIcon", SETTING_TYPE_BOOLEAN, 0, &g_ShowTrayIcon, static_cast<DOUBLE>(g_ShowTrayIcon) },
|
||||
// NOTE: AnimateZoom is misspelled, but since it is a user setting stored in the registry we must continue to misspell it.
|
||||
{ L"AnimnateZoom", SETTING_TYPE_BOOLEAN, 0, &g_AnimateZoom, static_cast<DOUBLE>(g_AnimateZoom) },
|
||||
{ L"SmoothImage", SETTING_TYPE_BOOLEAN, 0, &g_SmoothImage, static_cast<DOUBLE>(g_SmoothImage) },
|
||||
{ L"TelescopeZoomOut", SETTING_TYPE_BOOLEAN, 0, &g_TelescopeZoomOut, static_cast<DOUBLE>(g_TelescopeZoomOut) },
|
||||
{ L"SnapToGrid", SETTING_TYPE_BOOLEAN, 0, &g_SnapToGrid, static_cast<DOUBLE>(g_SnapToGrid) },
|
||||
{ L"ZoominSliderLevel", SETTING_TYPE_DWORD, 0, &g_SliderZoomLevel, static_cast<DOUBLE>(g_SliderZoomLevel) },
|
||||
|
||||
@@ -170,8 +170,6 @@ type_pMagSetFullscreenTransform pMagSetFullscreenTransform;
|
||||
type_pMagSetInputTransform pMagSetInputTransform;
|
||||
type_pMagShowSystemCursor pMagShowSystemCursor;
|
||||
type_pMagSetWindowFilterList pMagSetWindowFilterList;
|
||||
type_MagSetFullscreenUseBitmapSmoothing pMagSetFullscreenUseBitmapSmoothing;
|
||||
type_pMagSetLensUseBitmapSmoothing pMagSetLensUseBitmapSmoothing;
|
||||
type_pMagInitialize pMagInitialize;
|
||||
type_pDwmIsCompositionEnabled pDwmIsCompositionEnabled;
|
||||
type_pGetPointerType pGetPointerType;
|
||||
@@ -1101,8 +1099,6 @@ void DrawHighlightedShape( DWORD Shape, HDC hdcScreenCompat, Gdiplus::Brush *pBr
|
||||
// Create a new bitmap that's the size of the area covered by the line + 2 * g_PenWidth
|
||||
Gdiplus::Rect lineBounds(min(x1, x2), min(y1, y2), abs(x2 - x1), abs(y2 - y1));
|
||||
|
||||
OutputDebug(L"DrawHighlightedShape\n");
|
||||
|
||||
// Expand for line drawing
|
||||
if (Shape == DRAW_LINE)
|
||||
lineBounds.Inflate(static_cast<int>(g_PenWidth / 2), static_cast<int>(g_PenWidth / 2));
|
||||
@@ -1190,7 +1186,7 @@ void DrawHighlightedShape( DWORD Shape, HDC hdcScreenCompat, Gdiplus::Brush *pBr
|
||||
DeleteDC(hdcDIBOrig);
|
||||
|
||||
// Invalidate the updated rectangle
|
||||
//InvalidateGdiplusRect(hWnd, lineBounds);
|
||||
// InvalidateGdiplusRect(hWnd, lineBounds);
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
@@ -1288,12 +1284,7 @@ void ScaleImage( HDC hdcDst, float xDst, float yDst, float wDst, float hDst,
|
||||
{
|
||||
Gdiplus::Bitmap srcBitmap( bmSrc, NULL );
|
||||
|
||||
// Use high quality interpolation when smooth image is enabled
|
||||
if (g_SmoothImage) {
|
||||
dstGraphics.SetInterpolationMode( Gdiplus::InterpolationModeHighQuality );
|
||||
} else {
|
||||
dstGraphics.SetInterpolationMode( Gdiplus::InterpolationModeLowQuality );
|
||||
}
|
||||
dstGraphics.SetInterpolationMode( Gdiplus::InterpolationModeLowQuality );
|
||||
dstGraphics.SetPixelOffsetMode( Gdiplus::PixelOffsetModeHalf );
|
||||
|
||||
dstGraphics.DrawImage( &srcBitmap, Gdiplus::RectF(xDst,yDst,wDst,hDst), xSrc, ySrc, wSrc, hSrc, Gdiplus::UnitPixel );
|
||||
@@ -2080,8 +2071,6 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
|
||||
IsAutostartConfigured() ? BST_CHECKED: BST_UNCHECKED );
|
||||
CheckDlgButton( g_OptionsTabs[ZOOM_PAGE].hPage, IDC_ANIMATE_ZOOM,
|
||||
g_AnimateZoom ? BST_CHECKED: BST_UNCHECKED );
|
||||
CheckDlgButton( g_OptionsTabs[ZOOM_PAGE].hPage, IDC_SMOOTH_IMAGE,
|
||||
g_SmoothImage ? BST_CHECKED: BST_UNCHECKED );
|
||||
|
||||
SendMessage( GetDlgItem(g_OptionsTabs[ZOOM_PAGE].hPage, IDC_ZOOM_SLIDER), TBM_SETRANGE, false, MAKELONG(0,_countof(g_ZoomLevels)-1) );
|
||||
SendMessage( GetDlgItem(g_OptionsTabs[ZOOM_PAGE].hPage, IDC_ZOOM_SLIDER), TBM_SETPOS, true, g_SliderZoomLevel );
|
||||
@@ -2221,7 +2210,6 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
|
||||
}
|
||||
g_ShowTrayIcon = IsDlgButtonChecked( hDlg, IDC_SHOW_TRAY_ICON ) == BST_CHECKED;
|
||||
g_AnimateZoom = IsDlgButtonChecked( g_OptionsTabs[ZOOM_PAGE].hPage, IDC_ANIMATE_ZOOM ) == BST_CHECKED;
|
||||
g_SmoothImage = IsDlgButtonChecked( g_OptionsTabs[ZOOM_PAGE].hPage, IDC_SMOOTH_IMAGE ) == BST_CHECKED;
|
||||
g_DemoTypeUserDriven = IsDlgButtonChecked( g_OptionsTabs[DEMOTYPE_PAGE].hPage, IDC_DEMOTYPE_USER_DRIVEN ) == BST_CHECKED;
|
||||
|
||||
newToggleKey = static_cast<DWORD>(SendMessage( GetDlgItem( g_OptionsTabs[ZOOM_PAGE].hPage, IDC_HOTKEY), HKM_GETHOTKEY, 0, 0 ));
|
||||
@@ -2735,6 +2723,7 @@ VOID DrawShape( DWORD Shape, HDC hDc, RECT *Rect, bool UseGdiPlus = false )
|
||||
bool isBlur = false;
|
||||
|
||||
Gdiplus::Graphics dstGraphics(hDc);
|
||||
|
||||
if( ( GetWindowLong( g_hWndMain, GWL_EXSTYLE ) & WS_EX_LAYERED ) == 0 )
|
||||
{
|
||||
dstGraphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
|
||||
@@ -2757,7 +2746,6 @@ VOID DrawShape( DWORD Shape, HDC hDc, RECT *Rect, bool UseGdiPlus = false )
|
||||
InflateRect(Rect, g_PenWidth / 2, g_PenWidth / 2);
|
||||
isBlur = true;
|
||||
}
|
||||
OutputDebug(L"Draw shape: highlight: %d pbrush: %d\n", PEN_COLOR_HIGHLIGHT(g_PenColor), pBrush != NULL);
|
||||
|
||||
switch (Shape) {
|
||||
case DRAW_RECTANGLE:
|
||||
@@ -2932,7 +2920,7 @@ void InvalidateCursorMoveArea( HWND hWnd, float zoomLevel, int width, int height
|
||||
{
|
||||
int x, y;
|
||||
RECT rc;
|
||||
int invWidth = g_PenWidth + CURSOR_SAVE_MARGIN;
|
||||
int invWidth = g_PenWidth;
|
||||
|
||||
if( DrawHighlightedCursor( zoomLevel, width, height ) ) {
|
||||
|
||||
@@ -2957,7 +2945,7 @@ void InvalidateCursorMoveArea( HWND hWnd, float zoomLevel, int width, int height
|
||||
void SaveCursorArea( HDC hDcTarget, HDC hDcSource, POINT pt )
|
||||
{
|
||||
OutputDebug( L"SaveCursorArea\n");
|
||||
int penWidth = g_PenWidth + CURSOR_SAVE_MARGIN;
|
||||
int penWidth = g_PenWidth + 2;
|
||||
BitBlt( hDcTarget, 0, 0, penWidth +CURSOR_ARM_LENGTH*2, penWidth +CURSOR_ARM_LENGTH*2,
|
||||
hDcSource, static_cast<INT> (pt.x- penWidth /2)-CURSOR_ARM_LENGTH,
|
||||
static_cast<INT>(pt.y- penWidth /2)-CURSOR_ARM_LENGTH, SRCCOPY|CAPTUREBLT );
|
||||
@@ -2971,7 +2959,7 @@ void SaveCursorArea( HDC hDcTarget, HDC hDcSource, POINT pt )
|
||||
void RestoreCursorArea( HDC hDcTarget, HDC hDcSource, POINT pt )
|
||||
{
|
||||
OutputDebug( L"RestoreCursorArea\n");
|
||||
int penWidth = g_PenWidth + CURSOR_SAVE_MARGIN;
|
||||
int penWidth = g_PenWidth + 2;
|
||||
BitBlt( hDcTarget, static_cast<INT>(pt.x- penWidth /2)-CURSOR_ARM_LENGTH,
|
||||
static_cast<INT>(pt.y- penWidth /2)-CURSOR_ARM_LENGTH, penWidth +CURSOR_ARM_LENGTH*2,
|
||||
penWidth + CURSOR_ARM_LENGTH*2, hDcSource, 0, 0, SRCCOPY|CAPTUREBLT );
|
||||
@@ -4190,11 +4178,6 @@ LRESULT APIENTRY MainWndProc(
|
||||
}
|
||||
#endif
|
||||
}
|
||||
OutputDebug(L"LIVEDRAW SMOOTHING: %d\n", g_SmoothImage);
|
||||
if (!pMagSetLensUseBitmapSmoothing(g_hWndLiveZoomMag, g_SmoothImage))
|
||||
{
|
||||
OutputDebug(L"MagSetLensUseBitmapSmoothing failed: %d\n", GetLastError());
|
||||
}
|
||||
|
||||
if ( g_RecordToggle )
|
||||
{
|
||||
@@ -5313,8 +5296,6 @@ LRESULT APIENTRY MainWndProc(
|
||||
|
||||
if( g_Drawing ) {
|
||||
|
||||
OutputDebug(L"Mousemove: Drawing\n");
|
||||
|
||||
POINT currentPt;
|
||||
|
||||
// Are we in pen mode on a tablet?
|
||||
@@ -5353,15 +5334,7 @@ LRESULT APIENTRY MainWndProc(
|
||||
}
|
||||
else
|
||||
{
|
||||
if (PEN_COLOR_HIGHLIGHT(g_PenColor))
|
||||
{
|
||||
// copy original bitmap to screen bitmap to erase previous highlight
|
||||
BitBlt(hdcScreenCompat, 0, 0, bmp.bmWidth, bmp.bmHeight, drawUndoList->hDc, 0, 0, SRCCOPY | CAPTUREBLT);
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawShape(g_DrawingShape, hdcScreenCompat, &g_rcRectangle, PEN_COLOR_HIGHLIGHT(g_PenColor));
|
||||
}
|
||||
DrawShape( g_DrawingShape, hdcScreenCompat, &g_rcRectangle );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5407,7 +5380,7 @@ LRESULT APIENTRY MainWndProc(
|
||||
g_rcRectangle.top != g_rcRectangle.bottom) {
|
||||
|
||||
// Draw the new target rectangle.
|
||||
DrawShape(g_DrawingShape, hdcScreenCompat, &g_rcRectangle, PEN_COLOR_HIGHLIGHT(g_PenColor));
|
||||
DrawShape(g_DrawingShape, hdcScreenCompat, &g_rcRectangle);
|
||||
OutputDebug(L"SHAPE: (%d, %d) - (%d, %d)\n", g_rcRectangle.left, g_rcRectangle.top,
|
||||
g_rcRectangle.right, g_rcRectangle.bottom);
|
||||
}
|
||||
@@ -5445,6 +5418,9 @@ LRESULT APIENTRY MainWndProc(
|
||||
Gdiplus::BitmapData* lineData = LockGdiPlusBitmap(lineBitmap);
|
||||
BYTE* pPixels = static_cast<BYTE*>(lineData->Scan0);
|
||||
|
||||
// Copy the contents of the screen bitmap to the temporary bitmap
|
||||
GetOldestUndo(drawUndoList);
|
||||
|
||||
// Create a GDI bitmap that's the size of the lineBounds rectangle
|
||||
Gdiplus::Bitmap *blurBitmap = CreateGdiplusBitmap( hdcScreenCompat, // oldestUndo->hDc,
|
||||
lineBounds.X, lineBounds.Y, lineBounds.Width, lineBounds.Height);
|
||||
@@ -5469,8 +5445,6 @@ LRESULT APIENTRY MainWndProc(
|
||||
}
|
||||
else if(PEN_COLOR_HIGHLIGHT(g_PenColor)) {
|
||||
|
||||
OutputDebug(L"HIGHLIGHT\n");
|
||||
|
||||
// This is a highlighting pen color
|
||||
Gdiplus::Rect lineBounds = GetLineBounds(prevPt, currentPt, g_PenWidth);
|
||||
Gdiplus::Bitmap* lineBitmap = DrawBitmapLine(lineBounds, prevPt, currentPt, &pen);
|
||||
@@ -5810,30 +5784,26 @@ LRESULT APIENTRY MainWndProc(
|
||||
if( !g_DrawingShape ) {
|
||||
|
||||
// If the point has changed, draw a line to it
|
||||
if (!PEN_COLOR_HIGHLIGHT(g_PenColor))
|
||||
{
|
||||
if (prevPt.x != LOWORD(lParam) || prevPt.y != HIWORD(lParam))
|
||||
if (prevPt.x != LOWORD(lParam) || prevPt.y != HIWORD(lParam)) {
|
||||
Gdiplus::Graphics dstGraphics(hdcScreenCompat);
|
||||
if ((GetWindowLong(g_hWndMain, GWL_EXSTYLE) & WS_EX_LAYERED) == 0)
|
||||
{
|
||||
Gdiplus::Graphics dstGraphics(hdcScreenCompat);
|
||||
if ((GetWindowLong(g_hWndMain, GWL_EXSTYLE) & WS_EX_LAYERED) == 0)
|
||||
{
|
||||
dstGraphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
|
||||
}
|
||||
Gdiplus::Color color = ColorFromColorRef(g_PenColor);
|
||||
Gdiplus::Pen pen(color, static_cast<Gdiplus::REAL>(g_PenWidth));
|
||||
Gdiplus::GraphicsPath path;
|
||||
pen.SetLineJoin(Gdiplus::LineJoinRound);
|
||||
path.AddLine(prevPt.x, prevPt.y, LOWORD(lParam), HIWORD(lParam));
|
||||
dstGraphics.DrawPath(&pen, &path);
|
||||
}
|
||||
// Draw a dot at the current point, if the point hasn't changed
|
||||
else
|
||||
{
|
||||
MoveToEx(hdcScreenCompat, prevPt.x, prevPt.y, NULL);
|
||||
LineTo(hdcScreenCompat, LOWORD(lParam), HIWORD(lParam));
|
||||
InvalidateRect(hWnd, NULL, FALSE);
|
||||
dstGraphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
|
||||
}
|
||||
Gdiplus::Color color = ColorFromColorRef(g_PenColor);
|
||||
Gdiplus::Pen pen(color, static_cast<Gdiplus::REAL>(g_PenWidth));
|
||||
Gdiplus::GraphicsPath path;
|
||||
pen.SetLineJoin(Gdiplus::LineJoinRound);
|
||||
path.AddLine(prevPt.x, prevPt.y, LOWORD(lParam), HIWORD(lParam));
|
||||
dstGraphics.DrawPath(&pen, &path);
|
||||
}
|
||||
// Draw a dot at the current point, if the point hasn't changed
|
||||
else {
|
||||
MoveToEx(hdcScreenCompat, prevPt.x, prevPt.y, NULL);
|
||||
LineTo(hdcScreenCompat, LOWORD(lParam), HIWORD(lParam));
|
||||
InvalidateRect(hWnd, NULL, FALSE);
|
||||
}
|
||||
|
||||
prevPt.x = LOWORD( lParam );
|
||||
prevPt.y = HIWORD( lParam );
|
||||
|
||||
@@ -5848,11 +5818,8 @@ LRESULT APIENTRY MainWndProc(
|
||||
g_rcRectangle.left != g_rcRectangle.right ) {
|
||||
|
||||
// erase previous
|
||||
if (!PEN_COLOR_HIGHLIGHT(g_PenColor))
|
||||
{
|
||||
SetROP2(hdcScreenCompat, R2_NOTXORPEN);
|
||||
DrawShape(g_DrawingShape, hdcScreenCompat, &g_rcRectangle);
|
||||
}
|
||||
SetROP2(hdcScreenCompat, R2_NOTXORPEN);
|
||||
DrawShape( g_DrawingShape, hdcScreenCompat, &g_rcRectangle );
|
||||
|
||||
// Draw the final shape
|
||||
HBRUSH hBrush = static_cast<HBRUSH>(GetStockObject( NULL_BRUSH ));
|
||||
@@ -6218,14 +6185,8 @@ LRESULT APIENTRY MainWndProc(
|
||||
SetStretchBltMode( hInterimSaveDc, HALFTONE );
|
||||
SetStretchBltMode( hSaveDc, HALFTONE );
|
||||
#else
|
||||
// Use HALFTONE for better quality when smooth image is enabled
|
||||
if (g_SmoothImage) {
|
||||
SetStretchBltMode( hInterimSaveDc, HALFTONE );
|
||||
SetStretchBltMode( hSaveDc, HALFTONE );
|
||||
} else {
|
||||
SetStretchBltMode( hInterimSaveDc, COLORONCOLOR );
|
||||
SetStretchBltMode( hSaveDc, COLORONCOLOR );
|
||||
}
|
||||
SetStretchBltMode( hInterimSaveDc, COLORONCOLOR );
|
||||
SetStretchBltMode( hSaveDc, COLORONCOLOR );
|
||||
#endif
|
||||
StretchBlt( hInterimSaveDc,
|
||||
0, 0,
|
||||
@@ -6348,12 +6309,7 @@ LRESULT APIENTRY MainWndProc(
|
||||
#if SCALE_HALFTONE
|
||||
SetStretchBltMode( hSaveDc, HALFTONE );
|
||||
#else
|
||||
// Use HALFTONE for better quality when smooth image is enabled
|
||||
if (g_SmoothImage) {
|
||||
SetStretchBltMode( hSaveDc, HALFTONE );
|
||||
} else {
|
||||
SetStretchBltMode( hSaveDc, COLORONCOLOR );
|
||||
}
|
||||
SetStretchBltMode( hSaveDc, COLORONCOLOR );
|
||||
#endif
|
||||
StretchBlt( hSaveDc,
|
||||
0, 0,
|
||||
@@ -6690,8 +6646,8 @@ LRESULT APIENTRY MainWndProc(
|
||||
(float)x, (float)y,
|
||||
width/zoomLevel, height/zoomLevel );
|
||||
} else {
|
||||
// do a fast, less accurate render (but use smooth if enabled)
|
||||
SetStretchBltMode( hDc, g_SmoothImage ? HALFTONE : COLORONCOLOR );
|
||||
// do a fast, less accurate render
|
||||
SetStretchBltMode( hDc, HALFTONE );
|
||||
StretchBlt( ps.hdc,
|
||||
0, 0,
|
||||
bmp.bmWidth, bmp.bmHeight,
|
||||
@@ -6704,12 +6660,7 @@ LRESULT APIENTRY MainWndProc(
|
||||
#if SCALE_HALFTONE
|
||||
SetStretchBltMode( hDc, zoomLevel == zoomTelescopeTarget ? HALFTONE : COLORONCOLOR );
|
||||
#else
|
||||
// Use HALFTONE for better quality when smooth image is enabled
|
||||
if (g_SmoothImage) {
|
||||
SetStretchBltMode( hDc, HALFTONE );
|
||||
} else {
|
||||
SetStretchBltMode( hDc, COLORONCOLOR );
|
||||
}
|
||||
SetStretchBltMode( hDc, COLORONCOLOR );
|
||||
#endif
|
||||
StretchBlt( ps.hdc,
|
||||
0, 0,
|
||||
@@ -6732,7 +6683,7 @@ LRESULT APIENTRY MainWndProc(
|
||||
|
||||
BITMAP local_bmp;
|
||||
GetObject(g_hBackgroundBmp, sizeof(local_bmp), &local_bmp);
|
||||
SetStretchBltMode( hdcScreenCompat, g_SmoothImage ? HALFTONE : COLORONCOLOR );
|
||||
SetStretchBltMode( hdcScreenCompat, HALFTONE );
|
||||
if( g_BreakBackgroundStretch ) {
|
||||
StretchBlt( hdcScreenCompat, 0, 0, width, height,
|
||||
g_hDcBackgroundFile, 0, 0, local_bmp.bmWidth, local_bmp.bmHeight, SRCCOPY|CAPTUREBLT );
|
||||
@@ -6891,6 +6842,7 @@ LRESULT CALLBACK LiveZoomWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM
|
||||
WS_CHILD | MS_SHOWMAGNIFIEDCURSOR | WS_VISIBLE,
|
||||
0, 0, 0, 0, hWnd, NULL, g_hInstance, NULL );
|
||||
}
|
||||
|
||||
ShowWindow( hWnd, SW_SHOW );
|
||||
InvalidateRect( g_hWndLiveZoomMag, NULL, TRUE );
|
||||
|
||||
@@ -7603,10 +7555,6 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance
|
||||
"MagSetWindowTransform" );
|
||||
pMagSetFullscreenTransform = (type_pMagSetFullscreenTransform)GetProcAddress(LoadLibrarySafe(L"magnification.dll", DLL_LOAD_LOCATION_SYSTEM),
|
||||
"MagSetFullscreenTransform");
|
||||
pMagSetFullscreenUseBitmapSmoothing = (type_MagSetFullscreenUseBitmapSmoothing)GetProcAddress(LoadLibrarySafe(L"magnification.dll", DLL_LOAD_LOCATION_SYSTEM),
|
||||
"MagSetFullscreenUseBitmapSmoothing");
|
||||
pMagSetLensUseBitmapSmoothing = (type_pMagSetLensUseBitmapSmoothing)GetProcAddress(LoadLibrarySafe(L"magnification.dll", DLL_LOAD_LOCATION_SYSTEM),
|
||||
"MagSetLensUseBitmapSmoothing");
|
||||
pMagSetInputTransform = (type_pMagSetInputTransform)GetProcAddress(LoadLibrarySafe(L"magnification.dll", DLL_LOAD_LOCATION_SYSTEM),
|
||||
"MagSetInputTransform");
|
||||
pMagShowSystemCursor = (type_pMagShowSystemCursor)GetProcAddress(LoadLibrarySafe(L"magnification.dll", DLL_LOAD_LOCATION_SYSTEM),
|
||||
|
||||
@@ -95,7 +95,6 @@
|
||||
#define IDC_COPYRIGHT 1075
|
||||
#define IDC_PEN_WIDTH 1105
|
||||
#define IDC_TIMER 1106
|
||||
#define IDC_SMOOTH_IMAGE 1107
|
||||
#define IDC_SAVE 40002
|
||||
#define IDC_COPY 40004
|
||||
#define IDC_RECORD 40006
|
||||
@@ -110,7 +109,7 @@
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_NEXT_RESOURCE_VALUE 118
|
||||
#define _APS_NEXT_COMMAND_VALUE 40013
|
||||
#define _APS_NEXT_CONTROL_VALUE 1078
|
||||
#define _APS_NEXT_CONTROL_VALUE 1076
|
||||
#define _APS_NEXT_SYMED_VALUE 101
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -281,7 +281,21 @@ namespace Awake.Core
|
||||
TimeSpan remainingTime = expireAt - DateTimeOffset.Now;
|
||||
|
||||
Observable.Timer(remainingTime).Subscribe(
|
||||
_ => HandleTimerCompletion("expirable"),
|
||||
_ =>
|
||||
{
|
||||
Logger.LogInfo("Completed expirable keep-awake.");
|
||||
CancelExistingThread();
|
||||
|
||||
if (IsUsingPowerToysConfig)
|
||||
{
|
||||
SetPassiveKeepAwake();
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInfo("Exiting after expirable keep awake.");
|
||||
CompleteExit(Environment.ExitCode);
|
||||
}
|
||||
},
|
||||
_tokenSource.Token);
|
||||
}
|
||||
|
||||
@@ -334,46 +348,49 @@ namespace Awake.Core
|
||||
|
||||
SetModeShellIcon();
|
||||
|
||||
var targetExpiryTime = DateTimeOffset.Now.AddSeconds(seconds);
|
||||
ulong desiredDuration = (ulong)seconds * 1000;
|
||||
ulong targetDuration = Math.Min(desiredDuration, uint.MaxValue - 1) / 1000;
|
||||
|
||||
Observable.Interval(TimeSpan.FromSeconds(1))
|
||||
.Select(_ => targetExpiryTime - DateTimeOffset.Now)
|
||||
.TakeWhile(remaining => remaining.TotalSeconds > 0)
|
||||
.Subscribe(
|
||||
remainingTimeSpan =>
|
||||
if (desiredDuration > uint.MaxValue)
|
||||
{
|
||||
Logger.LogInfo($"The desired interval of {seconds} seconds ({desiredDuration}ms) exceeds the limit. Defaulting to maximum possible value: {targetDuration} seconds. Read more about existing limits in the official documentation: https://aka.ms/powertoys/awake");
|
||||
}
|
||||
|
||||
IObservable<long> timerObservable = Observable.Timer(TimeSpan.FromSeconds(targetDuration));
|
||||
IObservable<long> intervalObservable = Observable.Interval(TimeSpan.FromSeconds(1)).TakeUntil(timerObservable);
|
||||
IObservable<long> combinedObservable = Observable.CombineLatest(intervalObservable, timerObservable.StartWith(0), (elapsedSeconds, _) => elapsedSeconds + 1);
|
||||
|
||||
combinedObservable.Subscribe(
|
||||
elapsedSeconds =>
|
||||
{
|
||||
TimeRemaining = (uint)targetDuration - (uint)elapsedSeconds;
|
||||
if (TimeRemaining >= 0)
|
||||
{
|
||||
TimeRemaining = (uint)remainingTimeSpan.TotalSeconds;
|
||||
|
||||
TrayHelper.SetShellIcon(
|
||||
TrayHelper.WindowHandle,
|
||||
$"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_TIMED}][{ScreenStateString}][{remainingTimeSpan.ToHumanReadableString()}]",
|
||||
$"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_TIMED}][{ScreenStateString}][{TimeSpan.FromSeconds(TimeRemaining).ToHumanReadableString()}]",
|
||||
TrayHelper.TimedIcon,
|
||||
TrayIconAction.Update);
|
||||
},
|
||||
_ => HandleTimerCompletion("timed"),
|
||||
_tokenSource.Token);
|
||||
}
|
||||
}
|
||||
},
|
||||
() =>
|
||||
{
|
||||
Logger.LogInfo("Completed timed thread.");
|
||||
CancelExistingThread();
|
||||
|
||||
/// <summary>
|
||||
/// Handles the common logic that should execute when a keep-awake timer completes. Resets
|
||||
/// the application state to Passive if configured; otherwise it exits.
|
||||
/// </summary>
|
||||
private static void HandleTimerCompletion(string timerType)
|
||||
{
|
||||
Logger.LogInfo($"Completed {timerType} keep-awake.");
|
||||
CancelExistingThread();
|
||||
|
||||
if (IsUsingPowerToysConfig)
|
||||
{
|
||||
// If running under PowerToys settings, just revert to the default Passive state.
|
||||
SetPassiveKeepAwake();
|
||||
}
|
||||
else
|
||||
{
|
||||
// If running as a standalone process, exit cleanly.
|
||||
Logger.LogInfo($"Exiting after {timerType} keep-awake.");
|
||||
CompleteExit(Environment.ExitCode);
|
||||
}
|
||||
if (IsUsingPowerToysConfig)
|
||||
{
|
||||
// If we're using PowerToys settings, we need to make sure that
|
||||
// we just switch over the Passive Keep-Awake.
|
||||
SetPassiveKeepAwake();
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInfo("Exiting after timed keep-awake.");
|
||||
CompleteExit(Environment.ExitCode);
|
||||
}
|
||||
},
|
||||
_tokenSource.Token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,153 +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.Runtime.CompilerServices;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Storage.FileSystem;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Helpers;
|
||||
|
||||
public static class PathHelper
|
||||
{
|
||||
public static bool Exists(string path, out bool isDirectory)
|
||||
{
|
||||
isDirectory = false;
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string? fullPath;
|
||||
try
|
||||
{
|
||||
fullPath = Path.GetFullPath(path);
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentException or IOException or UnauthorizedAccessException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var result = ExistsCore(fullPath, out isDirectory);
|
||||
if (result && IsDirectorySeparator(fullPath[^1]))
|
||||
{
|
||||
// Some sys-calls remove all trailing slashes and may give false positives for existing files.
|
||||
// We want to make sure that if the path ends in a trailing slash, it's truly a directory.
|
||||
return isDirectory;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalize potential local/UNC file path text input: trim whitespace and surrounding quotes.
|
||||
/// Windows file paths cannot contain quotes, but user input can include them.
|
||||
/// </summary>
|
||||
public static string Unquote(string? text)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(text) ? (text ?? string.Empty) : text.Trim().Trim('"');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Quick heuristic to determine if the string looks like a Windows file path (UNC or drive-letter based).
|
||||
/// </summary>
|
||||
public static bool LooksLikeFilePath(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// UNC path
|
||||
if (path.StartsWith(@"\\", StringComparison.Ordinal))
|
||||
{
|
||||
// Win32 File Namespaces \\?\
|
||||
if (path.StartsWith(@"\\?\", StringComparison.Ordinal))
|
||||
{
|
||||
return IsSlow(path[4..]);
|
||||
}
|
||||
|
||||
// Basic UNC path validation: \\server\share or \\server\share\path
|
||||
var parts = path[2..].Split('\\', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
return parts.Length >= 2; // At minimum: server and share
|
||||
}
|
||||
|
||||
// Drive letter path (e.g., C:\ or C:)
|
||||
return path.Length >= 2 && char.IsLetter(path[0]) && path[1] == ':';
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates path syntax without performing any I/O by using Path.GetFullPath.
|
||||
/// </summary>
|
||||
public static bool HasValidPathSyntax(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_ = Path.GetFullPath(path);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a string represents a valid Windows file path (local or network)
|
||||
/// using fast syntax validation only. Reuses LooksLikeFilePath and HasValidPathSyntax.
|
||||
/// </summary>
|
||||
public static bool IsValidFilePath(string? path)
|
||||
{
|
||||
return LooksLikeFilePath(path) && HasValidPathSyntax(path);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsDirectorySeparator(char c)
|
||||
{
|
||||
return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar;
|
||||
}
|
||||
|
||||
private static bool ExistsCore(string fullPath, out bool isDirectory)
|
||||
{
|
||||
var attributes = PInvoke.GetFileAttributes(fullPath);
|
||||
var result = attributes != PInvoke.INVALID_FILE_ATTRIBUTES;
|
||||
isDirectory = result && (attributes & (uint)FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_DIRECTORY) != 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
public static bool IsSlow(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var root = Path.GetPathRoot(path);
|
||||
if (!string.IsNullOrEmpty(root))
|
||||
{
|
||||
if (root.Length > 2 && char.IsLetter(root[0]) && root[1] == ':')
|
||||
{
|
||||
return new DriveInfo(root).DriveType is not (DriveType.Fixed or DriveType.Ram);
|
||||
}
|
||||
else if (root.StartsWith(@"\\", StringComparison.Ordinal))
|
||||
{
|
||||
return !root.StartsWith(@"\\?\", StringComparison.Ordinal) || IsSlow(root[4..]);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,4 @@ MonitorFromWindow
|
||||
|
||||
SHOW_WINDOW_CMD
|
||||
ShellExecuteEx
|
||||
SEE_MASK_INVOKEIDLIST
|
||||
|
||||
GetFileAttributes
|
||||
FILE_FLAGS_AND_ATTRIBUTES
|
||||
INVALID_FILE_ATTRIBUTES
|
||||
SEE_MASK_INVOKEIDLIST
|
||||
@@ -1,12 +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.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Encapsulates a navigation request within Command Palette view models.
|
||||
/// </summary>
|
||||
/// <param name="TargetViewModel">A view model that should be navigated to.</param>
|
||||
/// <param name="NavigationToken"> A <see cref="CancellationToken"/> that can be used to cancel the pending navigation.</param>
|
||||
public record AsyncNavigationRequest(object? TargetViewModel, CancellationToken NavigationToken);
|
||||
@@ -2,10 +2,8 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
@@ -13,13 +11,6 @@ public partial class DetailsLinkViewModel(
|
||||
IDetailsElement _detailsElement,
|
||||
WeakReference<IPageContext> context) : DetailsElementViewModel(_detailsElement, context)
|
||||
{
|
||||
private static readonly string[] _initProperties = [
|
||||
nameof(Text),
|
||||
nameof(Link),
|
||||
nameof(IsLink),
|
||||
nameof(IsText),
|
||||
nameof(NavigateCommand)];
|
||||
|
||||
private readonly ExtensionObject<IDetailsLink> _dataModel =
|
||||
new(_detailsElement.Data as IDetailsLink);
|
||||
|
||||
@@ -31,8 +22,6 @@ public partial class DetailsLinkViewModel(
|
||||
|
||||
public bool IsText => !IsLink;
|
||||
|
||||
public RelayCommand? NavigateCommand { get; private set; }
|
||||
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
base.InitializeProperties();
|
||||
@@ -49,18 +38,9 @@ public partial class DetailsLinkViewModel(
|
||||
Text = Link.ToString();
|
||||
}
|
||||
|
||||
if (Link is not null)
|
||||
{
|
||||
// Custom command to open a link in the default browser or app,
|
||||
// depending on the link type.
|
||||
// Binding Link to a Hyperlink(Button).NavigateUri works only for
|
||||
// certain URI schemes (e.g., http, https) and cannot open file:
|
||||
// scheme URIs or local files.
|
||||
NavigateCommand = new RelayCommand(
|
||||
() => ShellHelpers.OpenInShell(Link.ToString()),
|
||||
() => Link is not null);
|
||||
}
|
||||
|
||||
UpdateProperty(_initProperties);
|
||||
UpdateProperty(nameof(Text));
|
||||
UpdateProperty(nameof(Link));
|
||||
UpdateProperty(nameof(IsLink));
|
||||
UpdateProperty(nameof(IsText));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,6 @@
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
|
||||
public record NavigateToPageMessage(PageViewModel Page, bool WithAnimation, CancellationToken CancellationToken);
|
||||
public record NavigateToPageMessage(PageViewModel Page, bool WithAnimation)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,8 +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.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
internal sealed partial class NullPageViewModel(TaskScheduler scheduler, AppExtensionHost extensionHost)
|
||||
: PageViewModel(null, scheduler, extensionHost);
|
||||
@@ -23,9 +23,6 @@ public partial class ShellViewModel : ObservableObject,
|
||||
private readonly Lock _invokeLock = new();
|
||||
private Task? _handleInvokeTask;
|
||||
|
||||
// Cancellation token source for page loading/navigation operations
|
||||
private CancellationTokenSource? _navigationCts;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsLoaded { get; set; } = false;
|
||||
|
||||
@@ -69,8 +66,6 @@ public partial class ShellViewModel : ObservableObject,
|
||||
|
||||
public bool IsNested => _isNested;
|
||||
|
||||
public PageViewModel NullPage { get; private set; }
|
||||
|
||||
public ShellViewModel(
|
||||
TaskScheduler scheduler,
|
||||
IRootPageService rootPageService,
|
||||
@@ -82,7 +77,6 @@ public partial class ShellViewModel : ObservableObject,
|
||||
_rootPageService = rootPageService;
|
||||
_appHostService = appHostService;
|
||||
|
||||
NullPage = new NullPageViewModel(_scheduler, appHostService.GetDefaultHost());
|
||||
_currentPage = new LoadingPageViewModel(null, _scheduler, appHostService.GetDefaultHost());
|
||||
|
||||
// Register to receive messages
|
||||
@@ -119,7 +113,7 @@ public partial class ShellViewModel : ObservableObject,
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task LoadPageViewModelAsync(PageViewModel viewModel, CancellationToken cancellationToken = default)
|
||||
public async Task LoadPageViewModelAsync(PageViewModel viewModel)
|
||||
{
|
||||
// Note: We removed the general loading state, extensions sometimes use their `IsLoading`, but it's inconsistently implemented it seems.
|
||||
// IsInitialized is our main indicator of the general overall state of loading props/items from a page we use for the progress bar
|
||||
@@ -131,80 +125,44 @@ public partial class ShellViewModel : ObservableObject,
|
||||
if (!viewModel.IsInitialized
|
||||
&& viewModel.InitializeCommand is not null)
|
||||
{
|
||||
var outer = Task.Run(
|
||||
async () =>
|
||||
{
|
||||
// You know, this creates the situation where we wait for
|
||||
// both loading page properties, AND the items, before we
|
||||
// display anything.
|
||||
//
|
||||
// We almost need to do an async await on initialize, then
|
||||
// just a fire-and-forget on FetchItems.
|
||||
// RE: We do set the CurrentPage in ShellPage.xaml.cs as well, so, we kind of are doing two different things here.
|
||||
// Definitely some more clean-up to do, but at least its centralized to one spot now.
|
||||
viewModel.InitializeCommand.Execute(null);
|
||||
|
||||
await viewModel.InitializeCommand.ExecutionTask!;
|
||||
|
||||
if (viewModel.InitializeCommand.ExecutionTask.Status != TaskStatus.RanToCompletion)
|
||||
{
|
||||
if (viewModel.InitializeCommand.ExecutionTask.Exception is AggregateException ex)
|
||||
{
|
||||
CoreLogger.LogError(ex.ToString());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var t = Task.Factory.StartNew(
|
||||
() =>
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
if (viewModel is IDisposable disposable)
|
||||
{
|
||||
try
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CoreLogger.LogError(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentPage = viewModel;
|
||||
},
|
||||
cancellationToken,
|
||||
TaskCreationOptions.None,
|
||||
_scheduler);
|
||||
await t;
|
||||
}
|
||||
},
|
||||
cancellationToken);
|
||||
await outer;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
var outer = Task.Run(async () =>
|
||||
{
|
||||
if (viewModel is IDisposable disposable)
|
||||
// You know, this creates the situation where we wait for
|
||||
// both loading page properties, AND the items, before we
|
||||
// display anything.
|
||||
//
|
||||
// We almost need to do an async await on initialize, then
|
||||
// just a fire-and-forget on FetchItems.
|
||||
// RE: We do set the CurrentPage in ShellPage.xaml.cs as well, so, we kind of are doing two different things here.
|
||||
// Definitely some more clean-up to do, but at least its centralized to one spot now.
|
||||
viewModel.InitializeCommand.Execute(null);
|
||||
|
||||
await viewModel.InitializeCommand.ExecutionTask!;
|
||||
|
||||
if (viewModel.InitializeCommand.ExecutionTask.Status != TaskStatus.RanToCompletion)
|
||||
{
|
||||
try
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
if (viewModel.InitializeCommand.ExecutionTask.Exception is AggregateException ex)
|
||||
{
|
||||
CoreLogger.LogError(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
var t = Task.Factory.StartNew(
|
||||
() =>
|
||||
{
|
||||
CurrentPage = viewModel;
|
||||
},
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.None,
|
||||
_scheduler);
|
||||
await t;
|
||||
}
|
||||
});
|
||||
await outer;
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentPage = viewModel;
|
||||
}
|
||||
}
|
||||
@@ -216,28 +174,6 @@ public partial class ShellViewModel : ObservableObject,
|
||||
|
||||
private void PerformCommand(PerformCommandMessage message)
|
||||
{
|
||||
// Create/replace the navigation cancellation token.
|
||||
// If one already exists, cancel and dispose it first.
|
||||
var newCts = new CancellationTokenSource();
|
||||
var oldCts = Interlocked.Exchange(ref _navigationCts, newCts);
|
||||
if (oldCts is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
oldCts.Cancel();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CoreLogger.LogError(ex.ToString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
oldCts.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
var navigationToken = newCts.Token;
|
||||
|
||||
var command = message.Command.Unsafe;
|
||||
if (command is null)
|
||||
{
|
||||
@@ -265,27 +201,16 @@ public partial class ShellViewModel : ObservableObject,
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
// Clear command bar, ViewModel initialization can already set new commands if it wants to
|
||||
OnUIThread(() => WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null)));
|
||||
|
||||
// Kick off async loading of our ViewModel
|
||||
LoadPageViewModelAsync(pageViewModel, navigationToken)
|
||||
LoadPageViewModelAsync(pageViewModel)
|
||||
.ContinueWith(
|
||||
(Task t) =>
|
||||
{
|
||||
// clean up the navigation token if it's still ours
|
||||
if (Interlocked.CompareExchange(ref _navigationCts, null, newCts) == newCts)
|
||||
{
|
||||
newCts.Dispose();
|
||||
}
|
||||
OnUIThread(() => { WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null)); });
|
||||
WeakReferenceMessenger.Default.Send<NavigateToPageMessage>(new(pageViewModel, message.WithAnimation));
|
||||
},
|
||||
navigationToken,
|
||||
TaskContinuationOptions.None,
|
||||
_scheduler);
|
||||
|
||||
// While we're loading in the background, immediately move to the next page.
|
||||
WeakReferenceMessenger.Default.Send<NavigateToPageMessage>(new(pageViewModel, message.WithAnimation, navigationToken));
|
||||
|
||||
// Note: Originally we set our page back in the ViewModel here, but that now happens in response to the Frame navigating triggered from the above
|
||||
// See RootFrame_Navigated event handler.
|
||||
}
|
||||
@@ -443,9 +368,4 @@ public partial class ShellViewModel : ObservableObject,
|
||||
TaskCreationOptions.None,
|
||||
_scheduler);
|
||||
}
|
||||
|
||||
public void CancelNavigation()
|
||||
{
|
||||
_navigationCts?.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,6 @@ using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.Ext.Apps;
|
||||
using Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
using Microsoft.CmdPal.Ext.Apps.State;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
@@ -38,7 +36,6 @@ public partial class MainListPage : DynamicListPage,
|
||||
private List<Scored<IListItem>>? _filteredItems;
|
||||
private List<Scored<IListItem>>? _filteredApps;
|
||||
private List<Scored<IListItem>>? _fallbackItems;
|
||||
private IEnumerable<Scored<IListItem>>? _scoredFallbackItems;
|
||||
private bool _includeApps;
|
||||
private bool _filteredItemsIncludesApps;
|
||||
private int _appResultLimit = 10;
|
||||
@@ -163,7 +160,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
{
|
||||
lock (_tlcManager.TopLevelCommands)
|
||||
{
|
||||
var limitedApps = new List<Scored<IListItem>>();
|
||||
List<Scored<IListItem>> limitedApps = new List<Scored<IListItem>>();
|
||||
|
||||
// Fuzzy matching can produce a lot of results, so we want to limit the
|
||||
// number of apps we show at once if it's a large set.
|
||||
@@ -174,7 +171,6 @@ public partial class MainListPage : DynamicListPage,
|
||||
|
||||
var items = Enumerable.Empty<Scored<IListItem>>()
|
||||
.Concat(_filteredItems is not null ? _filteredItems : [])
|
||||
.Concat(_scoredFallbackItems is not null ? _scoredFallbackItems : [])
|
||||
.Concat(limitedApps)
|
||||
.OrderByDescending(o => o.Score)
|
||||
|
||||
@@ -188,14 +184,6 @@ public partial class MainListPage : DynamicListPage,
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearResults()
|
||||
{
|
||||
_filteredItems = null;
|
||||
_filteredApps = null;
|
||||
_fallbackItems = null;
|
||||
_scoredFallbackItems = null;
|
||||
}
|
||||
|
||||
public override void UpdateSearchText(string oldSearch, string newSearch)
|
||||
{
|
||||
var timer = new Stopwatch();
|
||||
@@ -228,7 +216,8 @@ public partial class MainListPage : DynamicListPage,
|
||||
lock (_tlcManager.TopLevelCommands)
|
||||
{
|
||||
_filteredItemsIncludesApps = _includeApps;
|
||||
ClearResults();
|
||||
_filteredItems = null;
|
||||
_filteredApps = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,36 +233,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
var commands = _tlcManager.TopLevelCommands;
|
||||
lock (commands)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// prefilter fallbacks
|
||||
var specialFallbacks = new List<TopLevelViewModel>(_specialFallbacks.Length);
|
||||
var commonFallbacks = new List<TopLevelViewModel>();
|
||||
|
||||
foreach (var s in commands)
|
||||
{
|
||||
if (!s.IsFallback)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_specialFallbacks.Contains(s.CommandProviderId))
|
||||
{
|
||||
specialFallbacks.Add(s);
|
||||
}
|
||||
else
|
||||
{
|
||||
commonFallbacks.Add(s);
|
||||
}
|
||||
}
|
||||
|
||||
// start update of fallbacks; update special fallbacks separately,
|
||||
// so they can finish faster
|
||||
UpdateFallbacks(SearchText, specialFallbacks, token);
|
||||
UpdateFallbacks(SearchText, commonFallbacks, token);
|
||||
UpdateFallbacks(SearchText, commands.ToImmutableArray(), token);
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
@@ -284,7 +244,9 @@ public partial class MainListPage : DynamicListPage,
|
||||
if (string.IsNullOrEmpty(newSearch))
|
||||
{
|
||||
_filteredItemsIncludesApps = _includeApps;
|
||||
ClearResults();
|
||||
_filteredItems = null;
|
||||
_filteredApps = null;
|
||||
_fallbackItems = null;
|
||||
RaiseItemsChanged(commands.Count);
|
||||
return;
|
||||
}
|
||||
@@ -293,13 +255,17 @@ public partial class MainListPage : DynamicListPage,
|
||||
// re-use previous results. Reset _filteredItems, and keep er moving.
|
||||
if (!newSearch.StartsWith(oldSearch, StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
ClearResults();
|
||||
_filteredItems = null;
|
||||
_filteredApps = null;
|
||||
_fallbackItems = null;
|
||||
}
|
||||
|
||||
// If the internal state has changed, reset _filteredItems to reset the list.
|
||||
if (_filteredItemsIncludesApps != _includeApps)
|
||||
{
|
||||
ClearResults();
|
||||
_filteredItems = null;
|
||||
_filteredApps = null;
|
||||
_fallbackItems = null;
|
||||
}
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
@@ -307,9 +273,9 @@ public partial class MainListPage : DynamicListPage,
|
||||
return;
|
||||
}
|
||||
|
||||
var newFilteredItems = Enumerable.Empty<IListItem>();
|
||||
var newFallbacks = Enumerable.Empty<IListItem>();
|
||||
var newApps = Enumerable.Empty<IListItem>();
|
||||
IEnumerable<IListItem> newFilteredItems = Enumerable.Empty<IListItem>();
|
||||
IEnumerable<IListItem> newFallbacks = Enumerable.Empty<IListItem>();
|
||||
IEnumerable<IListItem> newApps = Enumerable.Empty<IListItem>();
|
||||
|
||||
if (_filteredItems is not null)
|
||||
{
|
||||
@@ -345,12 +311,15 @@ public partial class MainListPage : DynamicListPage,
|
||||
// with a list of all our commands & apps.
|
||||
if (!newFilteredItems.Any() && !newApps.Any())
|
||||
{
|
||||
newFilteredItems = commands.Where(s => !s.IsFallback);
|
||||
// We're going to start over with our fallbacks
|
||||
newFallbacks = Enumerable.Empty<IListItem>();
|
||||
|
||||
newFilteredItems = commands.Where(s => !s.IsFallback || _specialFallbacks.Contains(s.CommandProviderId));
|
||||
|
||||
// Fallbacks are always included in the list, even if they
|
||||
// don't match the search text. But we don't want to
|
||||
// consider them when filtering the list.
|
||||
newFallbacks = commonFallbacks;
|
||||
newFallbacks = commands.Where(s => s.IsFallback && !_specialFallbacks.Contains(s.CommandProviderId));
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
@@ -361,20 +330,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
|
||||
if (_includeApps)
|
||||
{
|
||||
var allNewApps = AllAppsCommandProvider.Page.GetItems().ToList();
|
||||
|
||||
// We need to remove pinned apps from allNewApps so they don't show twice.
|
||||
var pinnedApps = PinnedAppsManager.Instance.GetPinnedAppIdentifiers();
|
||||
|
||||
if (pinnedApps.Length > 0)
|
||||
{
|
||||
newApps = allNewApps.Where(w =>
|
||||
pinnedApps.IndexOf(((AppListItem)w).AppIdentifier) < 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
newApps = allNewApps;
|
||||
}
|
||||
newApps = AllAppsCommandProvider.Page.GetItems().ToList();
|
||||
}
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
@@ -383,25 +339,8 @@ public partial class MainListPage : DynamicListPage,
|
||||
}
|
||||
}
|
||||
|
||||
var history = _serviceProvider.GetService<AppStateModel>()!.RecentCommands!;
|
||||
Func<string, IListItem, int> scoreItem = (a, b) => { return ScoreTopLevelItem(a, b, history); };
|
||||
|
||||
// Produce a list of everything that matches the current filter.
|
||||
_filteredItems = [.. ListHelpers.FilterListWithScores<IListItem>(newFilteredItems ?? [], SearchText, scoreItem)];
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IEnumerable<IListItem> newFallbacksForScoring = commands.Where(s => s.IsFallback && _specialFallbacks.Contains(s.CommandProviderId));
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_scoredFallbackItems = ListHelpers.FilterListWithScores<IListItem>(newFallbacksForScoring ?? [], SearchText, scoreItem);
|
||||
_filteredItems = [.. ListHelpers.FilterListWithScores<IListItem>(newFilteredItems ?? [], SearchText, ScoreTopLevelItem)];
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
@@ -419,7 +358,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
// Produce a list of filtered apps with the appropriate limit
|
||||
if (newApps.Any())
|
||||
{
|
||||
var scoredApps = ListHelpers.FilterListWithScores<IListItem>(newApps, SearchText, scoreItem);
|
||||
var scoredApps = ListHelpers.FilterListWithScores<IListItem>(newApps, SearchText, ScoreTopLevelItem);
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
@@ -486,7 +425,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
// Almost verbatim ListHelpers.ScoreListItem, but also accounting for the
|
||||
// fact that we want fallback handlers down-weighted, so that they don't
|
||||
// _always_ show up first.
|
||||
internal static int ScoreTopLevelItem(string query, IListItem topLevelOrAppItem, IRecentCommandsManager history)
|
||||
private int ScoreTopLevelItem(string query, IListItem topLevelOrAppItem)
|
||||
{
|
||||
var title = topLevelOrAppItem.Title;
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
@@ -562,9 +501,10 @@ public partial class MainListPage : DynamicListPage,
|
||||
// here we add the recent command weight boost
|
||||
//
|
||||
// Otherwise something like `x` will still match everything you've run before
|
||||
var finalScore = matchSomething * 10;
|
||||
var finalScore = matchSomething;
|
||||
if (matchSomething > 0)
|
||||
{
|
||||
var history = _serviceProvider.GetService<AppStateModel>()!.RecentCommands;
|
||||
var recentWeightBoost = history.GetCommandHistoryWeight(id);
|
||||
finalScore += recentWeightBoost;
|
||||
}
|
||||
@@ -581,7 +521,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
AppStateModel.SaveState(state);
|
||||
}
|
||||
|
||||
private static string IdForTopLevelOrAppItem(IListItem topLevelOrAppItem)
|
||||
private string IdForTopLevelOrAppItem(IListItem topLevelOrAppItem)
|
||||
{
|
||||
if (topLevelOrAppItem is TopLevelViewModel topLevel)
|
||||
{
|
||||
|
||||
@@ -1,7 +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.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.CmdPal.UI.ViewModels.UnitTests")]
|
||||
@@ -7,7 +7,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class RecentCommandsManager : ObservableObject, IRecentCommandsManager
|
||||
public partial class RecentCommandsManager : ObservableObject
|
||||
{
|
||||
[JsonInclude]
|
||||
internal List<HistoryItem> History { get; set; } = [];
|
||||
@@ -80,10 +80,3 @@ public partial class RecentCommandsManager : ObservableObject, IRecentCommandsMa
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface IRecentCommandsManager
|
||||
{
|
||||
int GetCommandHistoryWeight(string commandId);
|
||||
|
||||
void AddHistoryItem(string commandId);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
|
||||
Background="Transparent"
|
||||
PreviewKeyDown="UserControl_PreviewKeyDown"
|
||||
mc:Ignorable="d">
|
||||
|
||||
@@ -21,7 +22,7 @@
|
||||
<ResourceDictionary>
|
||||
<cmdpalUI:KeyChordToStringConverter x:Key="KeyChordToStringConverter" />
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
<Thickness x:Key="DefaultContextMenuItemPadding">12,8,12,8</Thickness>
|
||||
|
||||
<cmdpalUI:ContextItemTemplateSelector
|
||||
x:Key="ContextItemTemplateSelector"
|
||||
Critical="{StaticResource CriticalContextMenuViewModelTemplate}"
|
||||
@@ -30,7 +31,7 @@
|
||||
|
||||
<!-- Template for context items in the context item menu -->
|
||||
<DataTemplate x:Key="DefaultContextMenuViewModelTemplate" x:DataType="coreViewModels:CommandContextItemViewModel">
|
||||
<Grid Padding="{StaticResource DefaultContextMenuItemPadding}" AutomationProperties.Name="{x:Bind Title, Mode=OneWay}">
|
||||
<Grid AutomationProperties.Name="{x:Bind Title, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="32" />
|
||||
<ColumnDefinition Width="*" />
|
||||
@@ -70,7 +71,7 @@
|
||||
|
||||
<!-- Template for context items flagged as critical -->
|
||||
<DataTemplate x:Key="CriticalContextMenuViewModelTemplate" x:DataType="coreViewModels:CommandContextItemViewModel">
|
||||
<Grid Padding="{StaticResource DefaultContextMenuItemPadding}" AutomationProperties.Name="{x:Bind Title, Mode=OneWay}">
|
||||
<Grid AutomationProperties.Name="{x:Bind Title, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="32" />
|
||||
<ColumnDefinition Width="*" />
|
||||
@@ -113,7 +114,7 @@
|
||||
<DataTemplate x:Key="SeparatorContextMenuViewModelTemplate" x:DataType="coreViewModels:SeparatorViewModel">
|
||||
<Rectangle
|
||||
Height="1"
|
||||
Margin="0,2,0,2"
|
||||
Margin="-16,-12,-12,-12"
|
||||
Fill="{ThemeResource MenuFlyoutSeparatorBackground}" />
|
||||
</DataTemplate>
|
||||
</ResourceDictionary>
|
||||
@@ -124,39 +125,35 @@
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<ListView
|
||||
x:Name="CommandsDropdown"
|
||||
MinWidth="248"
|
||||
Margin="0,4,0,2"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="CommandsDropdown_ItemClick"
|
||||
ItemTemplateSelector="{StaticResource ContextItemTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
||||
PreviewKeyDown="CommandsDropdown_PreviewKeyDown"
|
||||
SelectionMode="Single">
|
||||
<ListView.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
</Style>
|
||||
</ListView.ItemContainerStyle>
|
||||
<ListView.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
</ListView.ItemContainerTransitions>
|
||||
</ListView>
|
||||
<Border BorderBrush="{ThemeResource MenuFlyoutSeparatorBackground}" BorderThickness="0,0,0,1" />
|
||||
|
||||
<StackPanel x:Name="CommandsPanel">
|
||||
<ListView
|
||||
x:Name="CommandsDropdown"
|
||||
MinWidth="248"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="CommandsDropdown_ItemClick"
|
||||
ItemTemplateSelector="{StaticResource ContextItemTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
||||
PreviewKeyDown="CommandsDropdown_PreviewKeyDown"
|
||||
SelectionMode="Single">
|
||||
<ListView.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
<Setter Property="Padding" Value="12,8" />
|
||||
</Style>
|
||||
</ListView.ItemContainerStyle>
|
||||
<ListView.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
</ListView.ItemContainerTransitions>
|
||||
</ListView>
|
||||
</StackPanel>
|
||||
<TextBox
|
||||
x:Name="ContextFilterBox"
|
||||
x:Uid="ContextFilterBox"
|
||||
Margin="0"
|
||||
Padding="10,7,6,8"
|
||||
Background="{ThemeResource AcrylicBackgroundFillColorBaseBrush}"
|
||||
BorderThickness="0,0,0,2"
|
||||
CornerRadius="8, 8, 0, 0"
|
||||
Margin="4"
|
||||
IsTextScaleFactorEnabled="True"
|
||||
KeyDown="ContextFilterBox_KeyDown"
|
||||
PreviewKeyDown="ContextFilterBox_PreviewKeyDown"
|
||||
Style="{StaticResource SearchTextBoxStyle}"
|
||||
TextChanged="ContextFilterBox_TextChanged" />
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="ContextMenuOrder">
|
||||
@@ -165,11 +162,9 @@
|
||||
<ui:IsEqualStateTrigger Value="{x:Bind ViewModel.FilterOnTop, Mode=OneWay}" To="True" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="CommandsDropdown.(Grid.Row)" Value="1" />
|
||||
<Setter Target="CommandsPanel.(Grid.Row)" Value="1" />
|
||||
<Setter Target="ContextFilterBox.(Grid.Row)" Value="0" />
|
||||
<Setter Target="CommandsDropdown.Margin" Value="0, 3, 0, 4" />
|
||||
<Setter Target="ContextFilterBox.CornerRadius" Value="8, 8, 0, 0" />
|
||||
<Setter Target="ContextFilterBox.Margin" Value="0,0,0,-1" />
|
||||
<Setter Target="CommandsDropdown.Margin" Value="0, 0, 0, 4" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="FilterOnBottom">
|
||||
@@ -177,11 +172,9 @@
|
||||
<ui:IsEqualStateTrigger Value="{x:Bind ViewModel.FilterOnTop, Mode=OneWay}" To="False" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="CommandsDropdown.(Grid.Row)" Value="0" />
|
||||
<Setter Target="CommandsPanel.(Grid.Row)" Value="0" />
|
||||
<Setter Target="ContextFilterBox.(Grid.Row)" Value="1" />
|
||||
<Setter Target="CommandsDropdown.Margin" Value="0, 4, 0, 4" />
|
||||
<Setter Target="ContextFilterBox.CornerRadius" Value="0, 0, 8, 8" />
|
||||
<Setter Target="ContextFilterBox.Margin" Value="0,0,0,-2" />
|
||||
<Setter Target="CommandsDropdown.Margin" Value="0, 4, 0, 0" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
|
||||
@@ -46,18 +46,11 @@ public sealed partial class ContentPage : Page,
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
if (e.Parameter is not AsyncNavigationRequest navigationRequest)
|
||||
if (e.Parameter is ContentPageViewModel vm)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid navigation parameter: {nameof(e.Parameter)} must be {nameof(AsyncNavigationRequest)}");
|
||||
ViewModel = vm;
|
||||
}
|
||||
|
||||
if (navigationRequest.TargetViewModel is not ContentPageViewModel contentPageViewModel)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid navigation target: AsyncNavigationRequest.{nameof(AsyncNavigationRequest.TargetViewModel)} must be {nameof(ContentPageViewModel)}");
|
||||
}
|
||||
|
||||
ViewModel = contentPageViewModel;
|
||||
|
||||
if (!WeakReferenceMessenger.Default.IsRegistered<ActivateSelectedListItemMessage>(this))
|
||||
{
|
||||
WeakReferenceMessenger.Default.Register<ActivateSelectedListItemMessage>(this);
|
||||
@@ -78,12 +71,6 @@ public sealed partial class ContentPage : Page,
|
||||
WeakReferenceMessenger.Default.Unregister<ActivateSecondaryCommandMessage>(this);
|
||||
|
||||
// Clean-up event listeners
|
||||
if (e.NavigationMode != NavigationMode.New)
|
||||
{
|
||||
ViewModel?.SafeCleanup();
|
||||
CleanupHelper.Cleanup(this);
|
||||
}
|
||||
|
||||
ViewModel = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -59,18 +59,11 @@ public sealed partial class ListPage : Page,
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
if (e.Parameter is not AsyncNavigationRequest navigationRequest)
|
||||
if (e.Parameter is ListViewModel lvm)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid navigation parameter: {nameof(e.Parameter)} must be {nameof(AsyncNavigationRequest)}");
|
||||
ViewModel = lvm;
|
||||
}
|
||||
|
||||
if (navigationRequest.TargetViewModel is not ListViewModel listViewModel)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid navigation target: AsyncNavigationRequest.{nameof(AsyncNavigationRequest.TargetViewModel)} must be {nameof(ListViewModel)}");
|
||||
}
|
||||
|
||||
ViewModel = listViewModel;
|
||||
|
||||
if (e.NavigationMode == NavigationMode.Back
|
||||
|| (e.NavigationMode == NavigationMode.New && ItemView.Items.Count > 0))
|
||||
{
|
||||
|
||||
@@ -23,29 +23,23 @@ public sealed partial class LoadingPage : Page
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
if (e.Parameter is not AsyncNavigationRequest request)
|
||||
if (e.Parameter is ShellViewModel shellVM
|
||||
&& shellVM.LoadCommand is not null)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid navigation parameter: {nameof(e.Parameter)} must be {nameof(AsyncNavigationRequest)}");
|
||||
}
|
||||
// This will load the built-in commands, then navigate to the main page.
|
||||
// Once the mainpage loads, we'll start loading extensions.
|
||||
shellVM.LoadCommand.Execute(null);
|
||||
|
||||
if (request.TargetViewModel is not ShellViewModel shellVM)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid navigation target: AsyncNavigationRequest.{nameof(AsyncNavigationRequest.TargetViewModel)} must be {nameof(ShellViewModel)}");
|
||||
}
|
||||
|
||||
// This will load the built-in commands, then navigate to the main page.
|
||||
// Once the mainpage loads, we'll start loading extensions.
|
||||
shellVM.LoadCommand.Execute(null);
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await shellVM.LoadCommand.ExecutionTask!;
|
||||
|
||||
if (shellVM.LoadCommand.ExecutionTask.Status != TaskStatus.RanToCompletion)
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
// TODO: Handle failure case
|
||||
}
|
||||
});
|
||||
await shellVM.LoadCommand.ExecutionTask!;
|
||||
|
||||
if (shellVM.LoadCommand.ExecutionTask.Status != TaskStatus.RanToCompletion)
|
||||
{
|
||||
// TODO: Handle failure case
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
base.OnNavigatedTo(e);
|
||||
}
|
||||
|
||||
@@ -108,7 +108,6 @@
|
||||
Visibility="{x:Bind IsText, Mode=OneWay}" />
|
||||
<HyperlinkButton
|
||||
Padding="0"
|
||||
Command="{x:Bind NavigateCommand, Mode=OneWay}"
|
||||
NavigateUri="{x:Bind Link, Mode=OneWay}"
|
||||
Visibility="{x:Bind IsLink, Mode=OneWay}">
|
||||
<TextBlock Text="{x:Bind Text, Mode=OneWay}" TextWrapping="Wrap" />
|
||||
|
||||
@@ -95,7 +95,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
AddHandler(KeyDownEvent, new KeyEventHandler(ShellPage_OnKeyDown), false);
|
||||
AddHandler(PointerPressedEvent, new PointerEventHandler(ShellPage_OnPointerPressed), true);
|
||||
|
||||
RootFrame.Navigate(typeof(LoadingPage), new AsyncNavigationRequest(ViewModel, CancellationToken.None));
|
||||
RootFrame.Navigate(typeof(LoadingPage), ViewModel);
|
||||
|
||||
var pageAnnouncementFormat = ResourceLoaderInstance.GetString("ScreenReader_Announcement_NavigatedToPage0");
|
||||
_pageNavigatedAnnouncement = CompositeFormat.Parse(pageAnnouncementFormat);
|
||||
@@ -153,7 +153,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
ContentPageViewModel => typeof(ContentPage),
|
||||
_ => throw new NotSupportedException(),
|
||||
},
|
||||
new AsyncNavigationRequest(message.Page, message.CancellationToken),
|
||||
message.Page,
|
||||
message.WithAnimation ? DefaultPageAnimation : _noAnimation);
|
||||
|
||||
PowerToysTelemetry.Log.WriteEvent(new OpenPage(RootFrame.BackStackDepth));
|
||||
@@ -403,8 +403,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
{
|
||||
HideDetails();
|
||||
|
||||
ViewModel.CancelNavigation();
|
||||
|
||||
// Note: That we restore the VM state below in RootFrame_Navigated call back after this occurs.
|
||||
// In the future, we may want to manage the back stack ourselves vs. relying on Frame
|
||||
// We could replace Frame with a ContentPresenter, but then have to manage transition animations ourselves.
|
||||
@@ -458,32 +456,11 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
// This listens to the root frame to ensure that we also track the content's page VM as well that we passed as a parameter.
|
||||
// This is currently used for both forward and backward navigation.
|
||||
// As when we go back that we restore ourselves to the proper state within our VM
|
||||
if (e.Parameter is AsyncNavigationRequest request)
|
||||
if (e.Parameter is PageViewModel page)
|
||||
{
|
||||
if (request.NavigationToken.IsCancellationRequested && e.NavigationMode is not (Microsoft.UI.Xaml.Navigation.NavigationMode.Back or Microsoft.UI.Xaml.Navigation.NavigationMode.Forward))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (request.TargetViewModel)
|
||||
{
|
||||
case PageViewModel pageViewModel:
|
||||
ViewModel.CurrentPage = pageViewModel;
|
||||
break;
|
||||
case ShellViewModel:
|
||||
// This one is an exception, for now (LoadingPage is tied to ShellViewModel,
|
||||
// but ShellViewModel is not PageViewModel.
|
||||
ViewModel.CurrentPage = ViewModel.NullPage;
|
||||
break;
|
||||
default:
|
||||
ViewModel.CurrentPage = ViewModel.NullPage;
|
||||
Logger.LogWarning($"Invalid navigation target: AsyncNavigationRequest.{nameof(AsyncNavigationRequest.TargetViewModel)} must be {nameof(PageViewModel)}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning("Unrecognized target for shell navigation: " + e.Parameter);
|
||||
// Note, this shortcuts and fights a bit with our LoadPageViewModel above, but we want to better fast display and incrementally load anyway
|
||||
// We just need to reconcile our loading systems a bit more in the future.
|
||||
ViewModel.CurrentPage = page;
|
||||
}
|
||||
|
||||
if (e.Content is Page element)
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>Microsoft.CmdPal.UI.ViewModels.UnitTests</RootNamespace>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="MSTest" />
|
||||
<PackageReference Include="WyHash" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Microsoft.CmdPal.UI.ViewModels\Microsoft.CmdPal.UI.ViewModels.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,444 +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.Linq;
|
||||
using Microsoft.CmdPal.Ext.UnitTestBase;
|
||||
using Microsoft.CmdPal.UI.ViewModels.MainPage;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Windows.Foundation;
|
||||
using WyHash;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public partial class RecentCommandsTests : CommandPaletteUnitTestBase
|
||||
{
|
||||
private static RecentCommandsManager CreateHistory(IList<string>? commandIds = null)
|
||||
{
|
||||
var history = new RecentCommandsManager();
|
||||
if (commandIds != null)
|
||||
{
|
||||
foreach (var item in commandIds)
|
||||
{
|
||||
history.AddHistoryItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
return history;
|
||||
}
|
||||
|
||||
private static RecentCommandsManager CreateBasicHistoryService()
|
||||
{
|
||||
var commonCommands = new List<string>
|
||||
{
|
||||
"com.microsoft.cmdpal.shell",
|
||||
"com.microsoft.cmdpal.windowwalker",
|
||||
"Visual Studio 2022 Preview_6533433915015224980",
|
||||
"com.microsoft.cmdpal.reload",
|
||||
"com.microsoft.cmdpal.shell",
|
||||
};
|
||||
|
||||
return CreateHistory(commonCommands);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateHistoryFunctionality()
|
||||
{
|
||||
// Setup
|
||||
var history = CreateHistory();
|
||||
|
||||
// Act
|
||||
history.AddHistoryItem("com.microsoft.cmdpal.shell");
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(history.GetCommandHistoryWeight("com.microsoft.cmdpal.shell") > 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateHistoryWeighting()
|
||||
{
|
||||
// Setup
|
||||
var history = CreateBasicHistoryService();
|
||||
|
||||
// Act
|
||||
var shellWeight = history.GetCommandHistoryWeight("com.microsoft.cmdpal.shell");
|
||||
var windowWalkerWeight = history.GetCommandHistoryWeight("com.microsoft.cmdpal.windowwalker");
|
||||
var vsWeight = history.GetCommandHistoryWeight("Visual Studio 2022 Preview_6533433915015224980");
|
||||
var reloadWeight = history.GetCommandHistoryWeight("com.microsoft.cmdpal.reload");
|
||||
var nonExistentWeight = history.GetCommandHistoryWeight("non.existent.command");
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(shellWeight > windowWalkerWeight, "Shell should be weighted higher than Window Walker, more uses");
|
||||
Assert.IsTrue(vsWeight > windowWalkerWeight, "Visual Studio should be weighted higher than Window Walker, because recency");
|
||||
Assert.AreEqual(reloadWeight, vsWeight, "both reload and VS were used in the last three commands, same weight");
|
||||
Assert.IsTrue(shellWeight > vsWeight, "VS and run were both used in the last 3, but shell has 2 more frequency");
|
||||
Assert.AreEqual(0, nonExistentWeight, "Nonexistent command should have zero weight");
|
||||
}
|
||||
|
||||
private sealed partial record ListItemMock(
|
||||
string Title,
|
||||
string? Subtitle = "",
|
||||
string? GivenId = "",
|
||||
string? ProviderId = "") : IListItem
|
||||
{
|
||||
public string Id => string.IsNullOrEmpty(GivenId) ? GenerateId() : GivenId;
|
||||
|
||||
public IDetails Details => throw new System.NotImplementedException();
|
||||
|
||||
public string Section => throw new System.NotImplementedException();
|
||||
|
||||
public ITag[] Tags => throw new System.NotImplementedException();
|
||||
|
||||
public string TextToSuggest => throw new System.NotImplementedException();
|
||||
|
||||
public ICommand Command => new NoOpCommand() { Id = Id };
|
||||
|
||||
public IIconInfo Icon => throw new System.NotImplementedException();
|
||||
|
||||
public IContextItem[] MoreCommands => throw new System.NotImplementedException();
|
||||
|
||||
#pragma warning disable CS0067
|
||||
public event TypedEventHandler<object, IPropChangedEventArgs>? PropChanged;
|
||||
#pragma warning restore CS0067
|
||||
|
||||
private string GenerateId()
|
||||
{
|
||||
// Use WyHash64 to generate stable ID hashes.
|
||||
// manually seeding with 0, so that the hash is stable across launches
|
||||
var result = WyHash64.ComputeHash64(ProviderId + Title + Subtitle, seed: 0);
|
||||
return $"{ProviderId}{result}";
|
||||
}
|
||||
}
|
||||
|
||||
private static RecentCommandsManager CreateHistory(IList<ListItemMock> items)
|
||||
{
|
||||
var history = new RecentCommandsManager();
|
||||
foreach (var item in items)
|
||||
{
|
||||
history.AddHistoryItem(item.Id);
|
||||
}
|
||||
|
||||
return history;
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateMocksWork()
|
||||
{
|
||||
// Setup
|
||||
var items = new List<ListItemMock>
|
||||
{
|
||||
new("Command A", "Subtitle A", "idA", "providerA"),
|
||||
new("Command B", "Subtitle B", GivenId: "idB"),
|
||||
new("Command C", "Subtitle C", ProviderId: "providerC"),
|
||||
new("Command A", "Subtitle A", "idA", "providerA"), // Duplicate to test incrementing uses
|
||||
};
|
||||
|
||||
// Act
|
||||
var history = CreateHistory(items);
|
||||
|
||||
// Assert
|
||||
foreach (var item in items)
|
||||
{
|
||||
var weight = history.GetCommandHistoryWeight(item.Id);
|
||||
Assert.IsTrue(weight > 0, $"Item {item.Title} should have a weight greater than zero.");
|
||||
}
|
||||
|
||||
// Check that the duplicate item has a higher weight due to increased uses
|
||||
var weightA = history.GetCommandHistoryWeight("idA");
|
||||
var weightB = history.GetCommandHistoryWeight("idB");
|
||||
var weightC = history.GetCommandHistoryWeight(items[2].Id); // providerC generated ID
|
||||
Assert.IsTrue(weightA > weightB, "Item A should have a higher weight than Item B due to more uses.");
|
||||
Assert.IsTrue(weightA > weightC, "Item A should have a higher weight than Item C due to more uses.");
|
||||
Assert.AreEqual(weightC, weightB, "Item C and Item B were used in the last 3 commands");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateHistoryBuckets()
|
||||
{
|
||||
// Setup
|
||||
// (these will be checked in reverse order, so that A is the most recent)
|
||||
var items = new List<ListItemMock>
|
||||
{
|
||||
new("Command A", "Subtitle A", GivenId: "idA"), // #0 -> bucket 0
|
||||
new("Command B", "Subtitle B", GivenId: "idB"), // #1 -> bucket 0
|
||||
new("Command C", "Subtitle C", GivenId: "idC"), // #2 -> bucket 0
|
||||
new("Command D", "Subtitle D", GivenId: "idD"), // #3 -> bucket 1
|
||||
new("Command E", "Subtitle E", GivenId: "idE"), // #4 -> bucket 1
|
||||
new("Command F", "Subtitle F", GivenId: "idF"), // #5 -> bucket 1
|
||||
new("Command G", "Subtitle G", GivenId: "idG"), // #6 -> bucket 1
|
||||
new("Command H", "Subtitle H", GivenId: "idH"), // #7 -> bucket 1
|
||||
new("Command I", "Subtitle I", GivenId: "idI"), // #8 -> bucket 1
|
||||
new("Command J", "Subtitle J", GivenId: "idJ"), // #9 -> bucket 1
|
||||
new("Command K", "Subtitle K", GivenId: "idK"), // #10 -> bucket 1
|
||||
new("Command L", "Subtitle L", GivenId: "idL"), // #11 -> bucket 2
|
||||
new("Command M", "Subtitle M", GivenId: "idM"), // #12 -> bucket 2
|
||||
new("Command N", "Subtitle N", GivenId: "idN"), // #13 -> bucket 2
|
||||
new("Command O", "Subtitle O", GivenId: "idO"), // #14 -> bucket 2
|
||||
};
|
||||
|
||||
for (var i = items.Count; i <= 50; i++)
|
||||
{
|
||||
items.Add(new ListItemMock($"Command #{i}", GivenId: $"id{i}"));
|
||||
}
|
||||
|
||||
// Act
|
||||
var history = CreateHistory(items.Reverse<ListItemMock>().ToList());
|
||||
|
||||
// Assert
|
||||
// First three items should be in the top bucket
|
||||
var weightA = history.GetCommandHistoryWeight("idA");
|
||||
var weightB = history.GetCommandHistoryWeight("idB");
|
||||
var weightC = history.GetCommandHistoryWeight("idC");
|
||||
|
||||
Assert.AreEqual(weightA, weightB, "Items A and B were used in the last 3 commands");
|
||||
Assert.AreEqual(weightB, weightC, "Items B and C were used in the last 3 commands");
|
||||
|
||||
// Next eight items (3-10 inclusive) should be in the second bucket
|
||||
var weightD = history.GetCommandHistoryWeight("idD");
|
||||
var weightE = history.GetCommandHistoryWeight("idE");
|
||||
var weightF = history.GetCommandHistoryWeight("idF");
|
||||
var weightG = history.GetCommandHistoryWeight("idG");
|
||||
var weightH = history.GetCommandHistoryWeight("idH");
|
||||
var weightI = history.GetCommandHistoryWeight("idI");
|
||||
var weightJ = history.GetCommandHistoryWeight("idJ");
|
||||
var weightK = history.GetCommandHistoryWeight("idK");
|
||||
|
||||
Assert.AreEqual(weightD, weightE, "Items D and E were used in the last 10 commands");
|
||||
Assert.AreEqual(weightE, weightF, "Items E and F were used in the last 10 commands");
|
||||
Assert.AreEqual(weightF, weightG, "Items F and G were used in the last 10 commands");
|
||||
Assert.AreEqual(weightG, weightH, "Items G and H were used in the last 10 commands");
|
||||
Assert.AreEqual(weightH, weightI, "Items H and I were used in the last 10 commands");
|
||||
Assert.AreEqual(weightI, weightJ, "Items I and J were used in the last 10 commands");
|
||||
Assert.AreEqual(weightJ, weightK, "Items J and K were used in the last 10 commands");
|
||||
|
||||
// Items up to the 15th should be in the third bucket
|
||||
var weightL = history.GetCommandHistoryWeight("idL");
|
||||
var weightM = history.GetCommandHistoryWeight("idM");
|
||||
var weightN = history.GetCommandHistoryWeight("idN");
|
||||
var weightO = history.GetCommandHistoryWeight("idO");
|
||||
var weight15 = history.GetCommandHistoryWeight("id15");
|
||||
Assert.AreEqual(weightL, weightM, "Items L and M were used in the last 15 commands");
|
||||
Assert.AreEqual(weightM, weightN, "Items M and N were used in the last 15 commands");
|
||||
Assert.AreEqual(weightN, weightO, "Items N and O were used in the last 15 commands");
|
||||
Assert.AreEqual(weightO, weight15, "Items O and 15 were used in the last 15 commands");
|
||||
|
||||
// Items after that should be in the lowest buckets
|
||||
var weight0 = history.GetCommandHistoryWeight(items[0].Id);
|
||||
var weight3 = history.GetCommandHistoryWeight(items[3].Id);
|
||||
var weight11 = history.GetCommandHistoryWeight(items[11].Id);
|
||||
var weight16 = history.GetCommandHistoryWeight("id16");
|
||||
var weight20 = history.GetCommandHistoryWeight("id20");
|
||||
var weight30 = history.GetCommandHistoryWeight("id30");
|
||||
var weight40 = history.GetCommandHistoryWeight("id40");
|
||||
var weight49 = history.GetCommandHistoryWeight("id49");
|
||||
|
||||
Assert.IsTrue(weight0 > weight3);
|
||||
Assert.IsTrue(weight3 > weight11);
|
||||
Assert.IsTrue(weight11 > weight16);
|
||||
|
||||
Assert.AreEqual(weight16, weight20);
|
||||
Assert.AreEqual(weight20, weight30);
|
||||
Assert.IsTrue(weight30 > weight40);
|
||||
Assert.AreEqual(weight40, weight49);
|
||||
|
||||
// The 50th item has fallen out of the list now
|
||||
var weight50 = history.GetCommandHistoryWeight("id50");
|
||||
Assert.AreEqual(0, weight50, "Item 50 should have fallen out of the history list");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateSimpleScoring()
|
||||
{
|
||||
// Setup
|
||||
var items = new List<ListItemMock>
|
||||
{
|
||||
new("Command A", "Subtitle A", GivenId: "idA"), // #0 -> bucket 0
|
||||
new("Command B", "Subtitle B", GivenId: "idB"), // #1 -> bucket 0
|
||||
new("Command C", "Subtitle C", GivenId: "idC"), // #2 -> bucket 0
|
||||
};
|
||||
|
||||
var history = CreateHistory(items.Reverse<ListItemMock>().ToList());
|
||||
|
||||
var scoreA = MainListPage.ScoreTopLevelItem("C", items[0], history);
|
||||
var scoreB = MainListPage.ScoreTopLevelItem("C", items[1], history);
|
||||
var scoreC = MainListPage.ScoreTopLevelItem("C", items[2], history);
|
||||
|
||||
// Assert
|
||||
// All of these equally match the query, and they're all in the same bucket,
|
||||
// so they should all have the same score.
|
||||
Assert.AreEqual(scoreA, scoreB, "Items A and B should have the same score");
|
||||
Assert.AreEqual(scoreB, scoreC, "Items B and C should have the same score");
|
||||
}
|
||||
|
||||
private static List<ListItemMock> CreateMockHistoryItems()
|
||||
{
|
||||
var items = new List<ListItemMock>
|
||||
{
|
||||
new("Visual Studio 2022"), // #0 -> bucket 0
|
||||
new("Visual Studio Code"), // #1 -> bucket 0
|
||||
new("Explore Mastodon", GivenId: "social.mastodon.explore"), // #2 -> bucket 0
|
||||
new("Run commands", Subtitle: "Executes commands (e.g. ping, cmd)", GivenId: "com.microsoft.cmdpal.run"), // #3 -> bucket 1
|
||||
new("Windows Settings"), // #4 -> bucket 1
|
||||
new("Command Prompt"), // #5 -> bucket 1
|
||||
new("Terminal Canary"), // #6 -> bucket 1
|
||||
};
|
||||
return items;
|
||||
}
|
||||
|
||||
private static RecentCommandsManager CreateMockHistoryService(List<ListItemMock>? items = null)
|
||||
{
|
||||
var history = CreateHistory((items ?? CreateMockHistoryItems()).Reverse<ListItemMock>().ToList());
|
||||
return history;
|
||||
}
|
||||
|
||||
private sealed record ScoredItem(ListItemMock Item, int Score)
|
||||
{
|
||||
public string Title => Item.Title;
|
||||
|
||||
public override string ToString() => $"[{Score}]{Title}";
|
||||
}
|
||||
|
||||
private static IEnumerable<ScoredItem> TieScoresToMatches(List<ListItemMock> items, List<int> scores)
|
||||
{
|
||||
if (items.Count != scores.Count)
|
||||
{
|
||||
throw new ArgumentException("Items and scores must have the same number of elements");
|
||||
}
|
||||
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
yield return new ScoredItem(items[i], scores[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<ScoredItem> GetMatches(IEnumerable<ScoredItem> scoredItems)
|
||||
{
|
||||
var matches = scoredItems
|
||||
.Where(x => x.Score > 0)
|
||||
.OrderByDescending(x => x.Score)
|
||||
.ToList();
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
private static IEnumerable<ScoredItem> GetMatches(List<ListItemMock> items, List<int> scores)
|
||||
{
|
||||
return GetMatches(TieScoresToMatches(items, scores));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateScoredWeightingSimple()
|
||||
{
|
||||
var items = CreateMockHistoryItems();
|
||||
var emptyHistory = CreateMockHistoryService(new());
|
||||
var history = CreateMockHistoryService(items);
|
||||
|
||||
var unweightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("C", item, emptyHistory)).ToList();
|
||||
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("C", item, history)).ToList();
|
||||
Assert.AreEqual(unweightedScores.Count, weightedScores.Count, "Both score lists should have the same number of items");
|
||||
for (var i = 0; i < unweightedScores.Count; i++)
|
||||
{
|
||||
var unweighted = unweightedScores[i];
|
||||
var weighted = weightedScores[i];
|
||||
var item = items[i];
|
||||
if (item.Title.Contains('C', System.StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
Assert.IsTrue(unweighted >= 0, $"Item {item.Title} didn't match the query, so should have a weighted score of zero");
|
||||
Assert.IsTrue(weighted > unweighted, $"Item {item.Title} should have a higher weighted ({weighted}) score than unweighted ({unweighted})");
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.AreEqual(unweighted, 0, $"Item {item.Title} didn't match the query, so should have a weighted score of zero");
|
||||
Assert.AreEqual(unweighted, weighted);
|
||||
}
|
||||
}
|
||||
|
||||
var unweightedMatches = GetMatches(items, unweightedScores).ToList();
|
||||
Assert.AreEqual(4, unweightedMatches.Count);
|
||||
Assert.AreEqual("Command Prompt", unweightedMatches[0].Title, "Command Prompt should be the top match");
|
||||
Assert.AreEqual("Visual Studio Code", unweightedMatches[1].Title, "Visual Studio Code should be the second match");
|
||||
Assert.AreEqual("Terminal Canary", unweightedMatches[2].Title);
|
||||
Assert.AreEqual("Run commands", unweightedMatches[3].Title);
|
||||
|
||||
// Even after weighting for 1 use, Command Prompt should still be the top match.
|
||||
var weightedMatches = GetMatches(items, weightedScores).ToList();
|
||||
Assert.AreEqual(4, weightedMatches.Count);
|
||||
Assert.AreEqual("Command Prompt", weightedMatches[0].Title);
|
||||
Assert.AreEqual("Visual Studio Code", weightedMatches[1].Title);
|
||||
Assert.AreEqual("Terminal Canary", weightedMatches[2].Title);
|
||||
Assert.AreEqual("Run commands", weightedMatches[3].Title);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateTitlesAreMoreImportantThanHistory()
|
||||
{
|
||||
var items = CreateMockHistoryItems();
|
||||
var emptyHistory = CreateMockHistoryService(new());
|
||||
var history = CreateMockHistoryService(items);
|
||||
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("te", item, history)).ToList();
|
||||
var weightedMatches = GetMatches(items, weightedScores).ToList();
|
||||
|
||||
Assert.AreEqual(3, weightedMatches.Count, "Find Terminal, VsCode and Run commands");
|
||||
|
||||
// Terminal is in bucket 1, VS Code is in bucket 0, but Terminal matches
|
||||
// the title better
|
||||
Assert.AreEqual("Terminal Canary", weightedMatches[0].Title, "Terminal should be the top match, title match");
|
||||
Assert.AreEqual("Visual Studio Code", weightedMatches[1].Title, "VsCode does fuzzy match, but is less relevant than Terminal");
|
||||
Assert.AreEqual("Run commands", weightedMatches[2].Title, "run only matches on the subtitle");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateTitlesAreMoreImportantThanUsage()
|
||||
{
|
||||
var items = CreateMockHistoryItems();
|
||||
var emptyHistory = CreateMockHistoryService(new());
|
||||
var history = CreateMockHistoryService(items);
|
||||
|
||||
// Add extra uses of VS Code to try and push it above Terminal
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
history.AddHistoryItem(items[1].Id);
|
||||
}
|
||||
|
||||
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("te", item, history)).ToList();
|
||||
var weightedMatches = GetMatches(items, weightedScores).ToList();
|
||||
|
||||
Assert.AreEqual(3, weightedMatches.Count, "Find Terminal, VsCode and Run commands");
|
||||
|
||||
// Terminal is in bucket 1, VS Code is in bucket 0, but Terminal matches
|
||||
// the title better
|
||||
Assert.AreEqual("Terminal Canary", weightedMatches[0].Title, "Terminal should be the top match, title match");
|
||||
Assert.AreEqual("Visual Studio Code", weightedMatches[1].Title, "VsCode does fuzzy match, but is less relevant than Terminal");
|
||||
Assert.AreEqual("Run commands", weightedMatches[2].Title, "run only matches on the subtitle");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateUsageEventuallyHelps()
|
||||
{
|
||||
var items = CreateMockHistoryItems();
|
||||
var emptyHistory = CreateMockHistoryService(new());
|
||||
var history = CreateMockHistoryService(items);
|
||||
|
||||
// We're gonna run this test and keep adding more uses of VS Code till
|
||||
// it breaks past Command Prompt
|
||||
var vsCodeId = items[1].Id;
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
history.AddHistoryItem(vsCodeId);
|
||||
|
||||
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("C", item, history)).ToList();
|
||||
var weightedMatches = GetMatches(items, weightedScores).ToList();
|
||||
Assert.AreEqual(4, weightedMatches.Count);
|
||||
|
||||
var expectedCmdIndex = i < 5 ? 0 : 1;
|
||||
var expectedCodeIndex = i < 5 ? 1 : 0;
|
||||
Assert.AreEqual("Command Prompt", weightedMatches[expectedCmdIndex].Title);
|
||||
Assert.AreEqual("Visual Studio Code", weightedMatches[expectedCodeIndex].Title);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
public sealed partial class AppListItem : ListItem
|
||||
internal sealed partial class AppListItem : ListItem
|
||||
{
|
||||
private static readonly Tag _appTag = new("App");
|
||||
|
||||
|
||||
@@ -103,8 +103,7 @@ public class UWPApplication : IUWPApplication
|
||||
new CommandContextItem(
|
||||
new OpenFileCommand(Location)
|
||||
{
|
||||
Icon = new("\uE838"),
|
||||
Name = Resources.open_location,
|
||||
Name = Resources.open_containing_folder,
|
||||
})
|
||||
{
|
||||
RequestedShortcut = KeyChords.OpenFileLocation,
|
||||
|
||||
@@ -207,10 +207,7 @@ public class Win32Program : IProgram
|
||||
});
|
||||
|
||||
commands.Add(new CommandContextItem(
|
||||
new ShowFileInFolderCommand(!string.IsNullOrEmpty(LnkFilePath) ? LnkFilePath : FullPath)
|
||||
{
|
||||
Name = Resources.open_location,
|
||||
})
|
||||
new OpenFileCommand(ParentDirectory))
|
||||
{
|
||||
RequestedShortcut = KeyChords.OpenFileLocation,
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties {
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Resources {
|
||||
@@ -241,7 +241,7 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Open file location.
|
||||
/// Looks up a localized string similar to Open location.
|
||||
/// </summary>
|
||||
internal static string open_location {
|
||||
get {
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
<value>File</value>
|
||||
</data>
|
||||
<data name="open_location" xml:space="preserve">
|
||||
<value>Open file location</value>
|
||||
<value>Open location</value>
|
||||
</data>
|
||||
<data name="copy_path" xml:space="preserve">
|
||||
<value>Copy path</value>
|
||||
@@ -237,4 +237,4 @@
|
||||
<data name="limit_none" xml:space="preserve">
|
||||
<value>Unlimited</value>
|
||||
</data>
|
||||
</root>
|
||||
</root>
|
||||
|
||||
@@ -1,35 +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 Microsoft.CmdPal.Ext.ClipboardHistory.Models;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
|
||||
|
||||
/// <summary>
|
||||
/// Abstraction for providers that can extract metadata and offer actions for a clipboard context.
|
||||
/// </summary>
|
||||
internal interface IClipboardMetadataProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the section title to show in the UI for this provider's metadata.
|
||||
/// </summary>
|
||||
string SectionTitle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this provider can produce metadata for the given item.
|
||||
/// </summary>
|
||||
bool CanHandle(ClipboardItem item);
|
||||
|
||||
/// <summary>
|
||||
/// Returns metadata elements for the UI. Caller decides section grouping.
|
||||
/// </summary>
|
||||
IEnumerable<DetailsElement> GetDetails(ClipboardItem item);
|
||||
|
||||
/// <summary>
|
||||
/// Returns context actions to be appended to MoreCommands. Use unique IDs for de-duplication.
|
||||
/// </summary>
|
||||
IEnumerable<ProviderAction> GetActions(ClipboardItem item);
|
||||
}
|
||||
@@ -1,12 +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.
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
|
||||
|
||||
internal sealed record ImageMetadata(
|
||||
uint Width,
|
||||
uint Height,
|
||||
double DpiX,
|
||||
double DpiY,
|
||||
ulong? StorageSize);
|
||||
@@ -1,55 +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.Threading.Tasks;
|
||||
using Windows.Graphics.Imaging;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
|
||||
|
||||
internal static class ImageMetadataAnalyzer
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads image metadata from a RandomAccessStreamReference without decoding pixels.
|
||||
/// Returns oriented dimensions (EXIF rotation applied).
|
||||
/// </summary>
|
||||
public static async Task<ImageMetadata> GetAsync(RandomAccessStreamReference reference)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(reference);
|
||||
|
||||
using IRandomAccessStream ras = await reference.OpenReadAsync().AsTask().ConfigureAwait(false);
|
||||
var sizeBytes = TryGetSize(ras);
|
||||
|
||||
// BitmapDecoder does not decode pixel data unless you ask it to,
|
||||
// so this is fast and memory-friendly.
|
||||
var decoder = await BitmapDecoder.CreateAsync(ras).AsTask().ConfigureAwait(false);
|
||||
|
||||
// OrientedPixelWidth/Height account for EXIF orientation
|
||||
var width = decoder.OrientedPixelWidth;
|
||||
var height = decoder.OrientedPixelHeight;
|
||||
|
||||
return new ImageMetadata(
|
||||
Width: width,
|
||||
Height: height,
|
||||
DpiX: decoder.DpiX,
|
||||
DpiY: decoder.DpiY,
|
||||
StorageSize: sizeBytes);
|
||||
}
|
||||
|
||||
private static ulong? TryGetSize(IRandomAccessStream s)
|
||||
{
|
||||
try
|
||||
{
|
||||
// On file-backed streams this is accurate.
|
||||
// On some URI/virtual streams this may be unsupported or 0.
|
||||
var size = s.Size;
|
||||
return size == 0 ? (ulong?)0 : size;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +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 ManagedCommon;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
|
||||
|
||||
internal sealed class ImageMetadataProvider : IClipboardMetadataProvider
|
||||
{
|
||||
public string SectionTitle => "Image metadata";
|
||||
|
||||
public bool CanHandle(ClipboardItem item) => item.IsImage;
|
||||
|
||||
public IEnumerable<DetailsElement> GetDetails(ClipboardItem item)
|
||||
{
|
||||
var result = new List<DetailsElement>();
|
||||
if (!CanHandle(item) || item.ImageData is null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var metadata = ImageMetadataAnalyzer.GetAsync(item.ImageData).GetAwaiter().GetResult();
|
||||
|
||||
result.Add(new DetailsElement
|
||||
{
|
||||
Key = "Dimensions",
|
||||
Data = new DetailsLink($"{metadata.Width} x {metadata.Height}"),
|
||||
});
|
||||
result.Add(new DetailsElement
|
||||
{
|
||||
Key = "DPI",
|
||||
Data = new DetailsLink($"{metadata.DpiX:0.###} x {metadata.DpiY:0.###}"),
|
||||
});
|
||||
|
||||
if (metadata.StorageSize != null)
|
||||
{
|
||||
result.Add(new DetailsElement
|
||||
{
|
||||
Key = "Storage size",
|
||||
Data = new DetailsLink(SizeFormatter.FormatSize(metadata.StorageSize.Value)),
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogDebug("Failed to retrieve image metadata:" + ex);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public IEnumerable<ProviderAction> GetActions(ClipboardItem item) => [];
|
||||
}
|
||||
@@ -1,14 +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.
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
|
||||
|
||||
internal enum LineEndingType
|
||||
{
|
||||
None,
|
||||
Windows, // \r\n (CRLF)
|
||||
Unix, // \n (LF)
|
||||
Mac, // \r (CR)
|
||||
Mixed,
|
||||
}
|
||||
@@ -1,14 +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 Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an action exposed by a metadata provider.
|
||||
/// </summary>
|
||||
/// <param name="Id">Unique identifier for de-duplication (case-insensitive).</param>
|
||||
/// <param name="Action">The actual context menu item to be shown.</param>
|
||||
internal readonly record struct ProviderAction(string Id, CommandContextItem Action);
|
||||
@@ -1,49 +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.Globalization;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
|
||||
|
||||
/// <summary>
|
||||
/// Utility for formatting byte sizes to a human-readable string.
|
||||
/// </summary>
|
||||
internal static class SizeFormatter
|
||||
{
|
||||
private const long KB = 1024;
|
||||
private const long MB = 1024 * KB;
|
||||
private const long GB = 1024 * MB;
|
||||
|
||||
public static string FormatSize(long bytes)
|
||||
{
|
||||
return bytes switch
|
||||
{
|
||||
>= GB => string.Format(CultureInfo.CurrentCulture, "{0:F2} GB", (double)bytes / GB),
|
||||
>= MB => string.Format(CultureInfo.CurrentCulture, "{0:F2} MB", (double)bytes / MB),
|
||||
>= KB => string.Format(CultureInfo.CurrentCulture, "{0:F2} KB", (double)bytes / KB),
|
||||
_ => string.Format(CultureInfo.CurrentCulture, "{0} B", bytes),
|
||||
};
|
||||
}
|
||||
|
||||
public static string FormatSize(ulong bytes)
|
||||
{
|
||||
// Use double for division to avoid overflow; thresholds mirror long version
|
||||
if (bytes >= (ulong)GB)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, "{0:F2} GB", bytes / (double)GB);
|
||||
}
|
||||
|
||||
if (bytes >= (ulong)MB)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, "{0:F2} MB", bytes / (double)MB);
|
||||
}
|
||||
|
||||
if (bytes >= (ulong)KB)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, "{0:F2} KB", bytes / (double)KB);
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.CurrentCulture, "{0} B", bytes);
|
||||
}
|
||||
}
|
||||
@@ -1,138 +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;
|
||||
using System.IO;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
|
||||
|
||||
/// <summary>
|
||||
/// Detects when text content is a valid existing file or directory path and exposes basic metadata.
|
||||
/// </summary>
|
||||
internal sealed class TextFileSystemMetadataProvider : IClipboardMetadataProvider
|
||||
{
|
||||
public string SectionTitle => "File";
|
||||
|
||||
public bool CanHandle(ClipboardItem item)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(item);
|
||||
|
||||
if (!item.IsText || string.IsNullOrWhiteSpace(item.Content))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var text = PathHelper.Unquote(item.Content);
|
||||
return PathHelper.IsValidFilePath(text);
|
||||
}
|
||||
|
||||
public IEnumerable<DetailsElement> GetDetails(ClipboardItem item)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(item);
|
||||
|
||||
var result = new List<DetailsElement>();
|
||||
if (!item.IsText || string.IsNullOrWhiteSpace(item.Content))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var path = PathHelper.Unquote(item.Content);
|
||||
|
||||
if (PathHelper.IsSlow(path) || !PathHelper.Exists(path, out var isDirectory))
|
||||
{
|
||||
result.Add(new DetailsElement { Key = "Name", Data = new DetailsLink(Path.GetFileName(path)) });
|
||||
result.Add(new DetailsElement { Key = "Location", Data = new DetailsLink(UrlHelper.NormalizeUrl(path), path) });
|
||||
return result;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!isDirectory)
|
||||
{
|
||||
var fi = new FileInfo(path);
|
||||
result.Add(new DetailsElement { Key = "Name", Data = new DetailsLink(fi.Name) });
|
||||
result.Add(new DetailsElement { Key = "Location", Data = new DetailsLink(UrlHelper.NormalizeUrl(fi.FullName), fi.FullName) });
|
||||
result.Add(new DetailsElement { Key = "Type", Data = new DetailsLink(fi.Extension) });
|
||||
result.Add(new DetailsElement { Key = "Size", Data = new DetailsLink(SizeFormatter.FormatSize(fi.Length)) });
|
||||
result.Add(new DetailsElement { Key = "Modified", Data = new DetailsLink(fi.LastWriteTime.ToString(CultureInfo.CurrentCulture)) });
|
||||
result.Add(new DetailsElement { Key = "Created", Data = new DetailsLink(fi.CreationTime.ToString(CultureInfo.CurrentCulture)) });
|
||||
}
|
||||
else
|
||||
{
|
||||
var di = new DirectoryInfo(path);
|
||||
result.Add(new DetailsElement { Key = "Name", Data = new DetailsLink(di.Name) });
|
||||
result.Add(new DetailsElement { Key = "Location", Data = new DetailsLink(UrlHelper.NormalizeUrl(di.FullName), di.FullName) });
|
||||
result.Add(new DetailsElement { Key = "Type", Data = new DetailsLink("Folder") });
|
||||
result.Add(new DetailsElement { Key = "Modified", Data = new DetailsLink(di.LastWriteTime.ToString(CultureInfo.CurrentCulture)) });
|
||||
result.Add(new DetailsElement { Key = "Created", Data = new DetailsLink(di.CreationTime.ToString(CultureInfo.CurrentCulture)) });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to retrieve file system metadata.", ex);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public IEnumerable<ProviderAction> GetActions(ClipboardItem item)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(item);
|
||||
|
||||
if (!item.IsText || string.IsNullOrWhiteSpace(item.Content))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var path = PathHelper.Unquote(item.Content);
|
||||
|
||||
if (PathHelper.IsSlow(path) || !PathHelper.Exists(path, out var isDirectory))
|
||||
{
|
||||
// One anything
|
||||
var open = new CommandContextItem(new OpenFileCommand(path)) { RequestedShortcut = KeyChords.OpenUrl };
|
||||
yield return new ProviderAction(WellKnownActionIds.Open, open);
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (!isDirectory)
|
||||
{
|
||||
// Open file
|
||||
var open = new CommandContextItem(new OpenFileCommand(path)) { RequestedShortcut = KeyChords.OpenUrl };
|
||||
yield return new ProviderAction(WellKnownActionIds.Open, open);
|
||||
|
||||
// Show in folder (select)
|
||||
var show = new CommandContextItem(new ShowFileInFolderCommand(path)) { RequestedShortcut = WellKnownKeyChords.OpenFileLocation };
|
||||
yield return new ProviderAction(WellKnownActionIds.OpenLocation, show);
|
||||
|
||||
// Copy path
|
||||
var copy = new CommandContextItem(new CopyPathCommand(path)) { RequestedShortcut = WellKnownKeyChords.CopyFilePath };
|
||||
yield return new ProviderAction(WellKnownActionIds.CopyPath, copy);
|
||||
|
||||
// Open in console at file location
|
||||
var openConsole = new CommandContextItem(OpenInConsoleCommand.FromFile(path)) { RequestedShortcut = WellKnownKeyChords.OpenInConsole };
|
||||
yield return new ProviderAction(WellKnownActionIds.OpenConsole, openConsole);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Open folder
|
||||
var openFolder = new CommandContextItem(new OpenFileCommand(path)) { RequestedShortcut = KeyChords.OpenUrl };
|
||||
yield return new ProviderAction(WellKnownActionIds.Open, openFolder);
|
||||
|
||||
// Open in console
|
||||
var openConsole = new CommandContextItem(OpenInConsoleCommand.FromDirectory(path)) { RequestedShortcut = WellKnownKeyChords.OpenInConsole };
|
||||
yield return new ProviderAction(WellKnownActionIds.OpenConsole, openConsole);
|
||||
|
||||
// Copy path
|
||||
var copy = new CommandContextItem(new CopyPathCommand(path)) { RequestedShortcut = WellKnownKeyChords.CopyFilePath };
|
||||
yield return new ProviderAction(WellKnownActionIds.CopyPath, copy);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +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.
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
|
||||
|
||||
internal sealed record TextMetadata
|
||||
{
|
||||
public int CharacterCount { get; init; }
|
||||
|
||||
public int WordCount { get; init; }
|
||||
|
||||
public int SentenceCount { get; init; }
|
||||
|
||||
public int LineCount { get; init; }
|
||||
|
||||
public int ParagraphCount { get; init; }
|
||||
|
||||
public LineEndingType LineEnding { get; init; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Characters: {CharacterCount}, Words: {WordCount}, Sentences: {SentenceCount}, Lines: {LineCount}, Paragraphs: {ParagraphCount}, Line Ending: {LineEnding}";
|
||||
}
|
||||
}
|
||||
@@ -1,109 +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.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
|
||||
|
||||
internal partial class TextMetadataAnalyzer
|
||||
{
|
||||
public TextMetadata Analyze(string input)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(input);
|
||||
|
||||
return new TextMetadata
|
||||
{
|
||||
CharacterCount = input.Length,
|
||||
WordCount = CountWords(input),
|
||||
SentenceCount = CountSentences(input),
|
||||
LineCount = CountLines(input),
|
||||
ParagraphCount = CountParagraphs(input),
|
||||
LineEnding = DetectLineEnding(input),
|
||||
};
|
||||
}
|
||||
|
||||
private LineEndingType DetectLineEnding(string text)
|
||||
{
|
||||
var crlfCount = Regex.Matches(text, "\r\n").Count;
|
||||
var lfCount = Regex.Matches(text, "(?<!\r)\n").Count;
|
||||
var crCount = Regex.Matches(text, "\r(?!\n)").Count;
|
||||
|
||||
var endingTypes = (crlfCount > 0 ? 1 : 0) + (lfCount > 0 ? 1 : 0) + (crCount > 0 ? 1 : 0);
|
||||
|
||||
if (endingTypes > 1)
|
||||
{
|
||||
return LineEndingType.Mixed;
|
||||
}
|
||||
|
||||
if (crlfCount > 0)
|
||||
{
|
||||
return LineEndingType.Windows;
|
||||
}
|
||||
|
||||
if (lfCount > 0)
|
||||
{
|
||||
return LineEndingType.Unix;
|
||||
}
|
||||
|
||||
if (crCount > 0)
|
||||
{
|
||||
return LineEndingType.Mac;
|
||||
}
|
||||
|
||||
return LineEndingType.None;
|
||||
}
|
||||
|
||||
private int CountLines(string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return text.Count(c => c == '\n') + 1;
|
||||
}
|
||||
|
||||
private int CountParagraphs(string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var paragraphs = ParagraphsRegex()
|
||||
.Split(text)
|
||||
.Count(static p => !string.IsNullOrWhiteSpace(p));
|
||||
|
||||
return paragraphs > 0 ? paragraphs : 1;
|
||||
}
|
||||
|
||||
private int CountWords(string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Regex.Matches(text, @"\b\w+\b").Count;
|
||||
}
|
||||
|
||||
private int CountSentences(string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var matches = SentencesRegex().Matches(text);
|
||||
return matches.Count > 0 ? matches.Count : (text.Trim().Length > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"(\r?\n){2,}")]
|
||||
private static partial Regex ParagraphsRegex();
|
||||
|
||||
[GeneratedRegex(@"[.!?]+(?=\s|$)")]
|
||||
private static partial Regex SentencesRegex();
|
||||
}
|
||||
@@ -1,63 +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.Globalization;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
|
||||
|
||||
internal sealed class TextMetadataProvider : IClipboardMetadataProvider
|
||||
{
|
||||
public string SectionTitle => "Text statistics";
|
||||
|
||||
public bool CanHandle(ClipboardItem item) => item.IsText;
|
||||
|
||||
public IEnumerable<DetailsElement> GetDetails(ClipboardItem item)
|
||||
{
|
||||
var result = new List<DetailsElement>();
|
||||
if (!CanHandle(item) || string.IsNullOrEmpty(item.Content))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var r = new TextMetadataAnalyzer().Analyze(item.Content);
|
||||
|
||||
result.Add(new DetailsElement
|
||||
{
|
||||
Key = "Characters",
|
||||
Data = new DetailsLink(r.CharacterCount.ToString(CultureInfo.CurrentCulture)),
|
||||
});
|
||||
result.Add(new DetailsElement
|
||||
{
|
||||
Key = "Words",
|
||||
Data = new DetailsLink(r.WordCount.ToString(CultureInfo.CurrentCulture)),
|
||||
});
|
||||
result.Add(new DetailsElement
|
||||
{
|
||||
Key = "Sentences",
|
||||
Data = new DetailsLink(r.SentenceCount.ToString(CultureInfo.CurrentCulture)),
|
||||
});
|
||||
result.Add(new DetailsElement
|
||||
{
|
||||
Key = "Lines",
|
||||
Data = new DetailsLink(r.LineCount.ToString(CultureInfo.CurrentCulture)),
|
||||
});
|
||||
result.Add(new DetailsElement
|
||||
{
|
||||
Key = "Paragraphs",
|
||||
Data = new DetailsLink(r.ParagraphCount.ToString(CultureInfo.CurrentCulture)),
|
||||
});
|
||||
result.Add(new DetailsElement
|
||||
{
|
||||
Key = "Line Ending",
|
||||
Data = new DetailsLink(r.LineEnding.ToString()),
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public IEnumerable<ProviderAction> GetActions(ClipboardItem item) => [];
|
||||
}
|
||||
@@ -1,113 +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;
|
||||
using System.Linq;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
|
||||
|
||||
/// <summary>
|
||||
/// Detects web links in text and shows normalized URL and key parts.
|
||||
/// </summary>
|
||||
internal sealed class WebLinkMetadataProvider : IClipboardMetadataProvider
|
||||
{
|
||||
public string SectionTitle => "Link";
|
||||
|
||||
public bool CanHandle(ClipboardItem item)
|
||||
{
|
||||
if (!item.IsText || string.IsNullOrWhiteSpace(item.Content))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!UrlHelper.IsValidUrl(item.Content))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalized = UrlHelper.NormalizeUrl(item.Content);
|
||||
if (!Uri.TryCreate(normalized, UriKind.Absolute, out var uri))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Exclude file: scheme; it's handled by TextFileSystemMetadataProvider
|
||||
return !uri.Scheme.Equals(Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public IEnumerable<DetailsElement> GetDetails(ClipboardItem item)
|
||||
{
|
||||
var result = new List<DetailsElement>();
|
||||
if (!item.IsText || string.IsNullOrWhiteSpace(item.Content))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var normalized = UrlHelper.NormalizeUrl(item.Content);
|
||||
if (!Uri.TryCreate(normalized, UriKind.Absolute, out var uri))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
// Skip file: at runtime as well (defensive)
|
||||
if (uri.Scheme.Equals(Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
result.Add(new DetailsElement { Key = "URL", Data = new DetailsLink(normalized) });
|
||||
result.Add(new DetailsElement { Key = "Host", Data = new DetailsLink(uri.Host) });
|
||||
|
||||
if (!uri.IsDefaultPort)
|
||||
{
|
||||
result.Add(new DetailsElement { Key = "Port", Data = new DetailsLink(uri.Port.ToString(CultureInfo.CurrentCulture)) });
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(uri.AbsolutePath) && uri.AbsolutePath != "/")
|
||||
{
|
||||
result.Add(new DetailsElement { Key = "Path", Data = new DetailsLink(uri.AbsolutePath) });
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(uri.Query))
|
||||
{
|
||||
var q = uri.Query;
|
||||
var count = q.Count(static c => c == '&') + (q.Length > 1 ? 1 : 0);
|
||||
result.Add(new DetailsElement { Key = "Query params", Data = new DetailsLink(count.ToString(CultureInfo.CurrentCulture)) });
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(uri.Fragment))
|
||||
{
|
||||
result.Add(new DetailsElement { Key = "Fragment", Data = new DetailsLink(uri.Fragment) });
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore malformed inputs
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public IEnumerable<ProviderAction> GetActions(ClipboardItem item)
|
||||
{
|
||||
if (!CanHandle(item))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var normalized = UrlHelper.NormalizeUrl(item.Content!);
|
||||
|
||||
var open = new CommandContextItem(new OpenUrlCommand(normalized))
|
||||
{
|
||||
RequestedShortcut = KeyChords.OpenUrl,
|
||||
};
|
||||
yield return new ProviderAction(WellKnownActionIds.Open, open);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +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.
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
|
||||
|
||||
/// <summary>
|
||||
/// Well-known action id constants used to de-duplicate provider actions.
|
||||
/// </summary>
|
||||
internal static class WellKnownActionIds
|
||||
{
|
||||
public const string Open = "open";
|
||||
public const string OpenLocation = "openLocation";
|
||||
public const string CopyPath = "copyPath";
|
||||
public const string OpenConsole = "openConsole";
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||
using System.IO;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
|
||||
|
||||
@@ -31,7 +31,7 @@ internal static class UrlHelper
|
||||
}
|
||||
|
||||
// Check if it's a valid file path (local or network)
|
||||
if (PathHelper.IsValidFilePath(url))
|
||||
if (IsValidFilePath(url))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -78,7 +78,7 @@ internal static class UrlHelper
|
||||
url = url.Trim();
|
||||
|
||||
// If it's a valid file path, convert to file:// URI
|
||||
if (!url.StartsWith("file://", StringComparison.OrdinalIgnoreCase) && PathHelper.IsValidFilePath(url))
|
||||
if (IsValidFilePath(url) && !url.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -105,4 +105,40 @@ internal static class UrlHelper
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a string represents a valid file path (local or network)
|
||||
/// </summary>
|
||||
/// <param name="path">The string to check</param>
|
||||
/// <returns>True if the string is a valid file path, false otherwise</returns>
|
||||
private static bool IsValidFilePath(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Check for UNC paths (network paths starting with \\)
|
||||
if (path.StartsWith(@"\\", StringComparison.Ordinal))
|
||||
{
|
||||
// Basic UNC path validation: \\server\share or \\server\share\path
|
||||
var parts = path.Substring(2).Split('\\', StringSplitOptions.RemoveEmptyEntries);
|
||||
return parts.Length >= 2; // At minimum: server and share
|
||||
}
|
||||
|
||||
// Check for drive letters (C:\ or C:)
|
||||
if (path.Length >= 2 && char.IsLetter(path[0]) && path[1] == ':')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
<ProjectReference Include="..\..\Core\Microsoft.CmdPal.Core.Common\Microsoft.CmdPal.Core.Common.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -9,7 +9,6 @@ using System.Linq;
|
||||
using Microsoft.CmdPal.Common.Commands;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Commands;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
@@ -17,20 +16,13 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory.Models;
|
||||
|
||||
internal sealed partial class ClipboardListItem : ListItem
|
||||
{
|
||||
private static readonly IClipboardMetadataProvider[] MetadataProviders =
|
||||
[
|
||||
new ImageMetadataProvider(),
|
||||
new TextFileSystemMetadataProvider(),
|
||||
new WebLinkMetadataProvider(),
|
||||
new TextMetadataProvider(),
|
||||
];
|
||||
|
||||
private readonly SettingsManager _settingsManager;
|
||||
private readonly ClipboardItem _item;
|
||||
|
||||
private readonly CommandContextItem _deleteContextMenuItem;
|
||||
private readonly CommandContextItem? _pasteCommand;
|
||||
private readonly CommandContextItem? _copyCommand;
|
||||
private readonly CommandContextItem? _openUrlCommand;
|
||||
private readonly Lazy<Details> _lazyDetails;
|
||||
|
||||
public override IDetails? Details
|
||||
@@ -81,11 +73,26 @@ internal sealed partial class ClipboardListItem : ListItem
|
||||
|
||||
_pasteCommand = new CommandContextItem(new PasteCommand(_item, ClipboardFormat.Text, _settingsManager));
|
||||
_copyCommand = new CommandContextItem(new CopyCommand(_item, ClipboardFormat.Text));
|
||||
|
||||
// Check if the text content is a valid URL and add OpenUrl command
|
||||
if (UrlHelper.IsValidUrl(_item.Content ?? string.Empty))
|
||||
{
|
||||
var normalizedUrl = UrlHelper.NormalizeUrl(_item.Content ?? string.Empty);
|
||||
_openUrlCommand = new CommandContextItem(new OpenUrlCommand(normalizedUrl))
|
||||
{
|
||||
RequestedShortcut = KeyChords.OpenUrl,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
_openUrlCommand = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_pasteCommand = null;
|
||||
_copyCommand = null;
|
||||
_openUrlCommand = null;
|
||||
}
|
||||
|
||||
RefreshCommands();
|
||||
@@ -156,74 +163,27 @@ internal sealed partial class ClipboardListItem : ListItem
|
||||
commands.Add(firstCommand);
|
||||
}
|
||||
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var temp = new List<IContextItem>();
|
||||
foreach (var provider in MetadataProviders)
|
||||
if (_openUrlCommand != null)
|
||||
{
|
||||
if (!provider.CanHandle(_item))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var action in provider.GetActions(_item))
|
||||
{
|
||||
if (string.IsNullOrEmpty(action.Id) || !seen.Add(action.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
temp.Add(action.Action);
|
||||
}
|
||||
}
|
||||
|
||||
if (temp.Count > 0)
|
||||
{
|
||||
if (commands.Count > 0)
|
||||
{
|
||||
commands.Add(new Separator());
|
||||
}
|
||||
|
||||
commands.AddRange(temp);
|
||||
commands.Add(_openUrlCommand);
|
||||
}
|
||||
|
||||
commands.Add(new Separator());
|
||||
commands.Add(_deleteContextMenuItem);
|
||||
|
||||
return [.. commands];
|
||||
return commands.ToArray();
|
||||
}
|
||||
|
||||
private Details CreateDetails()
|
||||
{
|
||||
List<IDetailsElement> metadata = [];
|
||||
|
||||
foreach (var provider in MetadataProviders)
|
||||
{
|
||||
if (provider.CanHandle(_item))
|
||||
IDetailsElement[] metadata =
|
||||
[
|
||||
new DetailsElement
|
||||
{
|
||||
var details = provider.GetDetails(_item);
|
||||
if (details.Any())
|
||||
{
|
||||
metadata.Add(new DetailsElement
|
||||
{
|
||||
Key = provider.SectionTitle,
|
||||
Data = new DetailsSeparator(),
|
||||
});
|
||||
|
||||
metadata.AddRange(details);
|
||||
}
|
||||
Key = "Copied on",
|
||||
Data = new DetailsLink(_item.Timestamp.DateTime.ToString(DateTimeFormatInfo.CurrentInfo)),
|
||||
}
|
||||
}
|
||||
|
||||
metadata.Add(new DetailsElement
|
||||
{
|
||||
Key = "General",
|
||||
Data = new DetailsSeparator(),
|
||||
});
|
||||
metadata.Add(new DetailsElement
|
||||
{
|
||||
Key = "Copied",
|
||||
Data = new DetailsLink(_item.Timestamp.DateTime.ToString(DateTimeFormatInfo.CurrentInfo)),
|
||||
});
|
||||
];
|
||||
|
||||
if (_item.IsImage)
|
||||
{
|
||||
@@ -233,7 +193,7 @@ internal sealed partial class ClipboardListItem : ListItem
|
||||
{
|
||||
Title = _item.GetDataType(),
|
||||
HeroImage = heroImage,
|
||||
Metadata = [.. metadata],
|
||||
Metadata = metadata,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -243,7 +203,7 @@ internal sealed partial class ClipboardListItem : ListItem
|
||||
{
|
||||
Title = _item.GetDataType(),
|
||||
Body = $"```text\n{_item.Content}\n```",
|
||||
Metadata = [.. metadata],
|
||||
Metadata = metadata,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos
|
||||
private readonly Action<string>? _addToHistory;
|
||||
private readonly ITelemetryService _telemetryService;
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
private Task? _currentUpdateTask;
|
||||
|
||||
public FallbackExecuteItem(SettingsManager settings, Action<string>? addToHistory, ITelemetryService telemetryService)
|
||||
: base(
|
||||
@@ -39,22 +40,44 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos
|
||||
|
||||
try
|
||||
{
|
||||
DoUpdateQuery(query, cancellationToken);
|
||||
// Save the latest update task
|
||||
_currentUpdateTask = DoUpdateQueryAsync(query, cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// DO NOTHING HERE
|
||||
return;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Handle other exceptions
|
||||
return;
|
||||
}
|
||||
|
||||
// Await the task to ensure only the latest one gets processed
|
||||
_ = ProcessUpdateResultsAsync(_currentUpdateTask);
|
||||
}
|
||||
|
||||
private void DoUpdateQuery(string query, CancellationToken cancellationToken)
|
||||
private async Task ProcessUpdateResultsAsync(Task updateTask)
|
||||
{
|
||||
try
|
||||
{
|
||||
await updateTask;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Handle cancellation gracefully
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Handle other exceptions
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DoUpdateQueryAsync(string query, CancellationToken cancellationToken)
|
||||
{
|
||||
// Check for cancellation at the start
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var searchText = query.Trim();
|
||||
Expand(ref searchText);
|
||||
@@ -82,8 +105,22 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos
|
||||
using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
|
||||
var timeoutToken = combinedCts.Token;
|
||||
|
||||
exeExists = ShellListPageHelpers.FileExistInPath(exe, out fullExePath, cancellationToken);
|
||||
pathIsDir = Directory.Exists(exe);
|
||||
// Use Task.Run with timeout for file system operations
|
||||
var fileSystemTask = Task.Run(
|
||||
() =>
|
||||
{
|
||||
exeExists = ShellListPageHelpers.FileExistInPath(exe, out fullExePath);
|
||||
pathIsDir = Directory.Exists(exe);
|
||||
},
|
||||
CancellationToken.None);
|
||||
|
||||
// Wait for either completion or timeout
|
||||
await fileSystemTask.WaitAsync(timeoutToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Main cancellation token was cancelled, re-throw
|
||||
throw;
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
@@ -102,10 +139,7 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos
|
||||
}
|
||||
|
||||
// Check for cancellation before updating UI properties
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (exeExists)
|
||||
{
|
||||
@@ -138,10 +172,7 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos
|
||||
}
|
||||
|
||||
// Final cancellation check
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@@ -324,7 +324,7 @@ internal sealed class Window
|
||||
|
||||
// Correct the process data if the window belongs to a uwp app hosted by 'ApplicationFrameHost.exe'
|
||||
// (This only works if the window isn't minimized. For minimized windows the required child window isn't assigned.)
|
||||
if (_handlesToProcessCache[hWindow].IsUwpAppFrameHost)
|
||||
if (string.Equals(_handlesToProcessCache[hWindow].Name, "ApplicationFrameHost.exe", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
new Task(() =>
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@ internal sealed class WindowProcess
|
||||
/// <summary>
|
||||
/// An indicator if the window belongs to an 'Universal Windows Platform (UWP)' process
|
||||
/// </summary>
|
||||
private bool _isUwpAppFrameHost;
|
||||
private readonly bool _isUwpAppFrameHost;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the id of the process
|
||||
@@ -126,14 +126,6 @@ internal sealed class WindowProcess
|
||||
get; private set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the process (UWP app, packaged Win32 app, unpackaged Win32 app, ...).
|
||||
/// </summary>
|
||||
internal ProcessPackagingInfo ProcessType
|
||||
{
|
||||
get; private set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WindowProcess"/> class.
|
||||
/// </summary>
|
||||
@@ -142,10 +134,13 @@ internal sealed class WindowProcess
|
||||
/// <param name="name">New process name.</param>
|
||||
internal WindowProcess(uint pid, uint tid, string name)
|
||||
{
|
||||
ProcessType = ProcessPackagingInfo.Empty;
|
||||
UpdateProcessInfo(pid, tid, name);
|
||||
ProcessType = ProcessPackagingInspector.Inspect((int)pid);
|
||||
_isUwpAppFrameHost = string.Equals(Name, "ApplicationFrameHost.exe", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public ProcessPackagingInfo ProcessType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Updates the process information of the <see cref="WindowProcess"/> instance.
|
||||
/// </summary>
|
||||
@@ -161,10 +156,6 @@ internal sealed class WindowProcess
|
||||
|
||||
// Process can be elevated only if process id is not 0 (Dummy value on error)
|
||||
IsFullAccessDenied = (pid != 0) ? TestProcessAccessUsingAllAccessFlag(pid) : false;
|
||||
|
||||
// Update process type
|
||||
ProcessType = ProcessPackagingInspector.Inspect((int)pid);
|
||||
_isUwpAppFrameHost = string.Equals(Name, "ApplicationFrameHost.exe", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -11,13 +11,4 @@ internal sealed record ProcessPackagingInfo(
|
||||
bool IsAppContainer,
|
||||
string? PackageFullName,
|
||||
int? LastError
|
||||
)
|
||||
{
|
||||
public static ProcessPackagingInfo Empty { get; } = new(
|
||||
Pid: 0,
|
||||
Kind: ProcessPackagingKind.Unknown,
|
||||
HasPackageIdentity: false,
|
||||
IsAppContainer: false,
|
||||
PackageFullName: null,
|
||||
LastError: null);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -64,7 +64,7 @@ public class TerminalQuery : ITerminalQuery
|
||||
public IEnumerable<TerminalPackage> GetTerminals()
|
||||
{
|
||||
var user = WindowsIdentity.GetCurrent().User;
|
||||
var localAppDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
var localAppDataPath = Environment.GetEnvironmentVariable("LOCALAPPDATA");
|
||||
|
||||
foreach (var p in _packageManager.FindPackagesForUser(user.Value).Where(p => Packages.Contains(p.Id.Name)))
|
||||
{
|
||||
|
||||
@@ -1,37 +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.Threading;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace SamplePagesExtension;
|
||||
|
||||
internal sealed partial class SlowListPage : ListPage
|
||||
{
|
||||
public SlowListPage()
|
||||
{
|
||||
Icon = new IconInfo("\uEA79");
|
||||
Name = "Slow List Page";
|
||||
Title = "This page simulates a slow load";
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems()
|
||||
{
|
||||
Thread.Sleep(5000);
|
||||
|
||||
return [
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "This is a basic item in the list",
|
||||
Subtitle = "I don't do anything though",
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "This is another item in the list",
|
||||
Subtitle = "Still nothing",
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -48,11 +48,6 @@ public partial class SamplesListPage : ListPage
|
||||
Title = "Sample Icon Page",
|
||||
Subtitle = "A demo of using icons in various ways",
|
||||
},
|
||||
new ListItem(new SlowListPage())
|
||||
{
|
||||
Title = "Slow loading list page",
|
||||
Subtitle = "A demo of a list page that takes a while to load",
|
||||
},
|
||||
|
||||
// Content pages
|
||||
new ListItem(new SampleContentPage())
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 468 KiB |
@@ -1,36 +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 Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace PowerDisplay.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts boolean values to Visibility
|
||||
/// </summary>
|
||||
public partial class BoolToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is bool boolValue)
|
||||
{
|
||||
return boolValue ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is Visibility visibility)
|
||||
{
|
||||
return visibility == Visibility.Visible;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +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 Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace PowerDisplay.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts boolean values to their inverse
|
||||
/// </summary>
|
||||
public partial class InverseBoolConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is bool boolValue)
|
||||
{
|
||||
return !boolValue;
|
||||
}
|
||||
|
||||
return true; // Default to enabled if value is not bool
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is bool boolValue)
|
||||
{
|
||||
return !boolValue;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +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 Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace PowerDisplay.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts boolean values to Visibility (inverted)
|
||||
/// </summary>
|
||||
public partial class InverseBoolToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is bool boolValue)
|
||||
{
|
||||
return boolValue ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
return Visibility.Visible;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is Visibility visibility)
|
||||
{
|
||||
return visibility != Visibility.Visible;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,161 +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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using PowerDisplay.Core.Models;
|
||||
using Monitor = PowerDisplay.Core.Models.Monitor;
|
||||
|
||||
namespace PowerDisplay.Core.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitor controller interface
|
||||
/// </summary>
|
||||
public interface IMonitorController
|
||||
{
|
||||
/// <summary>
|
||||
/// Controller name
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Supported monitor type
|
||||
/// </summary>
|
||||
MonitorType SupportedType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the specified monitor can be controlled
|
||||
/// </summary>
|
||||
/// <param name="monitor">Monitor object</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Whether the monitor can be controlled</returns>
|
||||
Task<bool> CanControlMonitorAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets monitor brightness
|
||||
/// </summary>
|
||||
/// <param name="monitor">Monitor object</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Brightness information</returns>
|
||||
Task<BrightnessInfo> GetBrightnessAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Sets monitor brightness
|
||||
/// </summary>
|
||||
/// <param name="monitor">Monitor object</param>
|
||||
/// <param name="brightness">Brightness value (0-100)</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Operation result</returns>
|
||||
Task<MonitorOperationResult> SetBrightnessAsync(Monitor monitor, int brightness, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Discovers supported monitors
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>List of monitors</returns>
|
||||
Task<IEnumerable<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Validates monitor connection status
|
||||
/// </summary>
|
||||
/// <param name="monitor">Monitor object</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Whether the monitor is connected</returns>
|
||||
Task<bool> ValidateConnectionAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Releases resources
|
||||
/// </summary>
|
||||
void Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extended monitor controller interface (supports additional features)
|
||||
/// </summary>
|
||||
public interface IExtendedMonitorController : IMonitorController
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets monitor contrast
|
||||
/// </summary>
|
||||
Task<BrightnessInfo> GetContrastAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Sets monitor contrast
|
||||
/// </summary>
|
||||
Task<MonitorOperationResult> SetContrastAsync(Monitor monitor, int contrast, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets monitor volume
|
||||
/// </summary>
|
||||
Task<BrightnessInfo> GetVolumeAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Sets monitor volume
|
||||
/// </summary>
|
||||
Task<MonitorOperationResult> SetVolumeAsync(Monitor monitor, int volume, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets monitor capabilities string (DDC/CI)
|
||||
/// </summary>
|
||||
Task<string> GetCapabilitiesStringAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Saves current settings to monitor
|
||||
/// </summary>
|
||||
Task<MonitorOperationResult> SaveCurrentSettingsAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Monitor manager interface
|
||||
/// </summary>
|
||||
public interface IMonitorManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Currently detected monitors list
|
||||
/// </summary>
|
||||
IReadOnlyList<Monitor> Monitors { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Monitor list changed event
|
||||
/// </summary>
|
||||
event EventHandler<MonitorListChangedEventArgs>? MonitorsChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Monitor status changed event
|
||||
/// </summary>
|
||||
event EventHandler<MonitorStatusChangedEventArgs>? MonitorStatusChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Discovers all monitors
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets brightness of specified monitor
|
||||
/// </summary>
|
||||
Task<BrightnessInfo> GetBrightnessAsync(string monitorId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Sets brightness of specified monitor
|
||||
/// </summary>
|
||||
Task<MonitorOperationResult> SetBrightnessAsync(string monitorId, int brightness, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Sets brightness of all monitors
|
||||
/// </summary>
|
||||
Task<IEnumerable<MonitorOperationResult>> SetAllBrightnessAsync(int brightness, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes monitor status
|
||||
/// </summary>
|
||||
Task RefreshMonitorStatusAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets monitor by ID
|
||||
/// </summary>
|
||||
Monitor? GetMonitor(string monitorId);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +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 PowerDisplay.Core.Models;
|
||||
|
||||
namespace PowerDisplay.Core.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitor list changed event arguments
|
||||
/// </summary>
|
||||
public class MonitorListChangedEventArgs : EventArgs
|
||||
{
|
||||
public IReadOnlyList<Monitor> AddedMonitors { get; }
|
||||
|
||||
public IReadOnlyList<Monitor> RemovedMonitors { get; }
|
||||
|
||||
public IReadOnlyList<Monitor> AllMonitors { get; }
|
||||
|
||||
public MonitorListChangedEventArgs(
|
||||
IReadOnlyList<Monitor> addedMonitors,
|
||||
IReadOnlyList<Monitor> removedMonitors,
|
||||
IReadOnlyList<Monitor> allMonitors)
|
||||
{
|
||||
AddedMonitors = addedMonitors;
|
||||
RemovedMonitors = removedMonitors;
|
||||
AllMonitors = allMonitors;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,71 +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 PowerDisplay.Core.Models;
|
||||
|
||||
namespace PowerDisplay.Core.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitor status changed event arguments
|
||||
/// </summary>
|
||||
public class MonitorStatusChangedEventArgs : EventArgs
|
||||
{
|
||||
public Monitor Monitor { get; }
|
||||
|
||||
public int? OldBrightness { get; }
|
||||
|
||||
public int NewBrightness { get; }
|
||||
|
||||
public bool? OldAvailability { get; }
|
||||
|
||||
public bool NewAvailability { get; }
|
||||
|
||||
public string Message { get; }
|
||||
|
||||
public ChangeType Type { get; }
|
||||
|
||||
public enum ChangeType
|
||||
{
|
||||
Brightness,
|
||||
Contrast,
|
||||
Volume,
|
||||
ColorTemperature,
|
||||
Availability,
|
||||
General
|
||||
}
|
||||
|
||||
public MonitorStatusChangedEventArgs(
|
||||
Monitor monitor,
|
||||
int? oldBrightness,
|
||||
int newBrightness,
|
||||
bool? oldAvailability,
|
||||
bool newAvailability)
|
||||
{
|
||||
Monitor = monitor;
|
||||
OldBrightness = oldBrightness;
|
||||
NewBrightness = newBrightness;
|
||||
OldAvailability = oldAvailability;
|
||||
NewAvailability = newAvailability;
|
||||
Message = $"Brightness changed from {oldBrightness} to {newBrightness}";
|
||||
Type = ChangeType.Brightness;
|
||||
}
|
||||
|
||||
public MonitorStatusChangedEventArgs(
|
||||
Monitor monitor,
|
||||
string message,
|
||||
ChangeType changeType)
|
||||
{
|
||||
Monitor = monitor;
|
||||
Message = message;
|
||||
Type = changeType;
|
||||
|
||||
// Set defaults for compatibility
|
||||
OldBrightness = null;
|
||||
NewBrightness = monitor.CurrentBrightness;
|
||||
OldAvailability = null;
|
||||
NewAvailability = monitor.IsAvailable;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,88 +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;
|
||||
|
||||
namespace PowerDisplay.Core.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Brightness information structure
|
||||
/// </summary>
|
||||
public readonly struct BrightnessInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Current brightness value
|
||||
/// </summary>
|
||||
public int Current { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Minimum brightness value
|
||||
/// </summary>
|
||||
public int Minimum { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum brightness value
|
||||
/// </summary>
|
||||
public int Maximum { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the brightness information is valid
|
||||
/// </summary>
|
||||
public bool IsValid { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp when the brightness information was obtained
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; }
|
||||
|
||||
public BrightnessInfo(int current, int minimum, int maximum)
|
||||
{
|
||||
Current = current;
|
||||
Minimum = minimum;
|
||||
Maximum = maximum;
|
||||
IsValid = current >= minimum && current <= maximum && maximum > minimum;
|
||||
Timestamp = DateTime.Now;
|
||||
}
|
||||
|
||||
public BrightnessInfo(int current, int maximum)
|
||||
: this(current, 0, maximum)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates invalid brightness information
|
||||
/// </summary>
|
||||
public static BrightnessInfo Invalid => new(-1, -1, -1);
|
||||
|
||||
/// <summary>
|
||||
/// Converts brightness value to percentage (0-100)
|
||||
/// </summary>
|
||||
public int ToPercentage()
|
||||
{
|
||||
if (!IsValid || Maximum == Minimum)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return (int)Math.Round((double)(Current - Minimum) * 100 / (Maximum - Minimum));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates brightness value from percentage
|
||||
/// </summary>
|
||||
public int FromPercentage(int percentage)
|
||||
{
|
||||
if (!IsValid)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
percentage = Math.Clamp(percentage, 0, 100);
|
||||
return Minimum + (int)Math.Round((double)(Maximum - Minimum) * percentage / 100);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return IsValid ? $"{Current}/{Maximum} ({ToPercentage()}%)" : "Invalid";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
|
||||
namespace PowerDisplay.Core.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitor model that implements property change notification
|
||||
/// Thread-safe using Interlocked operations for concurrent access
|
||||
/// </summary>
|
||||
public class Monitor : INotifyPropertyChanged
|
||||
{
|
||||
private int _currentBrightness;
|
||||
private int _currentColorTemperature = 6500;
|
||||
private int _isAvailable = 1; // 1 = available, 0 = unavailable (for Interlocked)
|
||||
|
||||
/// <summary>
|
||||
/// Unique identifier (based on hardware ID)
|
||||
/// </summary>
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Hardware ID (EDID format like GSM5C6D)
|
||||
/// </summary>
|
||||
public string HardwareId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Display name
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Monitor type
|
||||
/// </summary>
|
||||
public MonitorType Type { get; set; } = MonitorType.Unknown;
|
||||
|
||||
/// <summary>
|
||||
/// Current brightness (0-100)
|
||||
/// Thread-safe using Interlocked.Exchange
|
||||
/// </summary>
|
||||
public int CurrentBrightness
|
||||
{
|
||||
get => Volatile.Read(ref _currentBrightness);
|
||||
set
|
||||
{
|
||||
var clamped = Math.Clamp(value, MinBrightness, MaxBrightness);
|
||||
var oldValue = Interlocked.Exchange(ref _currentBrightness, clamped);
|
||||
if (oldValue != clamped)
|
||||
{
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimum brightness value
|
||||
/// </summary>
|
||||
public int MinBrightness { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum brightness value
|
||||
/// </summary>
|
||||
public int MaxBrightness { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Current color temperature (2000-10000K)
|
||||
/// Thread-safe using Interlocked.Exchange
|
||||
/// </summary>
|
||||
public int CurrentColorTemperature
|
||||
{
|
||||
get => Volatile.Read(ref _currentColorTemperature);
|
||||
set
|
||||
{
|
||||
var clamped = Math.Clamp(value, MinColorTemperature, MaxColorTemperature);
|
||||
var oldValue = Interlocked.Exchange(ref _currentColorTemperature, clamped);
|
||||
if (oldValue != clamped)
|
||||
{
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimum color temperature value
|
||||
/// </summary>
|
||||
public int MinColorTemperature { get; set; } = 2000;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum color temperature value
|
||||
/// </summary>
|
||||
public int MaxColorTemperature { get; set; } = 10000;
|
||||
|
||||
/// <summary>
|
||||
/// Whether supports color temperature adjustment
|
||||
/// </summary>
|
||||
public bool SupportsColorTemperature { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether supports contrast adjustment
|
||||
/// </summary>
|
||||
public bool SupportsContrast => Capabilities.HasFlag(MonitorCapabilities.Contrast);
|
||||
|
||||
/// <summary>
|
||||
/// Whether supports volume adjustment (for audio-capable monitors)
|
||||
/// </summary>
|
||||
public bool SupportsVolume => Capabilities.HasFlag(MonitorCapabilities.Volume);
|
||||
|
||||
private int _currentContrast = 50;
|
||||
private int _currentVolume = 50;
|
||||
|
||||
/// <summary>
|
||||
/// Current contrast (0-100)
|
||||
/// Thread-safe using Interlocked.Exchange
|
||||
/// </summary>
|
||||
public int CurrentContrast
|
||||
{
|
||||
get => Volatile.Read(ref _currentContrast);
|
||||
set
|
||||
{
|
||||
var clamped = Math.Clamp(value, MinContrast, MaxContrast);
|
||||
var oldValue = Interlocked.Exchange(ref _currentContrast, clamped);
|
||||
if (oldValue != clamped)
|
||||
{
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimum contrast value
|
||||
/// </summary>
|
||||
public int MinContrast { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum contrast value
|
||||
/// </summary>
|
||||
public int MaxContrast { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Current volume (0-100)
|
||||
/// Thread-safe using Interlocked.Exchange
|
||||
/// </summary>
|
||||
public int CurrentVolume
|
||||
{
|
||||
get => Volatile.Read(ref _currentVolume);
|
||||
set
|
||||
{
|
||||
var clamped = Math.Clamp(value, MinVolume, MaxVolume);
|
||||
var oldValue = Interlocked.Exchange(ref _currentVolume, clamped);
|
||||
if (oldValue != clamped)
|
||||
{
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimum volume value
|
||||
/// </summary>
|
||||
public int MinVolume { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum volume value
|
||||
/// </summary>
|
||||
public int MaxVolume { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Whether available/online
|
||||
/// Thread-safe using Interlocked.Exchange
|
||||
/// </summary>
|
||||
public bool IsAvailable
|
||||
{
|
||||
get => Volatile.Read(ref _isAvailable) == 1;
|
||||
set
|
||||
{
|
||||
var newValue = value ? 1 : 0;
|
||||
var oldValue = Interlocked.Exchange(ref _isAvailable, newValue);
|
||||
if (oldValue != newValue)
|
||||
{
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Physical monitor handle (for DDC/CI)
|
||||
/// </summary>
|
||||
public IntPtr Handle { get; set; } = IntPtr.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Device path (for identification)
|
||||
/// </summary>
|
||||
public string DevicePath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Device key - unique identifier part of device path (like Twinkle Tray's deviceKey)
|
||||
/// </summary>
|
||||
public string DeviceKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Full device ID path (like Twinkle Tray's deviceID)
|
||||
/// </summary>
|
||||
public string DeviceID { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Instance name (used by WMI)
|
||||
/// </summary>
|
||||
public string InstanceName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Manufacturer information
|
||||
/// </summary>
|
||||
public string Manufacturer { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Connection type (HDMI, DP, VGA, etc.)
|
||||
/// </summary>
|
||||
public string ConnectionType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Communication method (DDC/CI, WMI, HDR API, etc.)
|
||||
/// </summary>
|
||||
public string CommunicationMethod { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Supported control methods
|
||||
/// </summary>
|
||||
public MonitorCapabilities Capabilities { get; set; } = MonitorCapabilities.None;
|
||||
|
||||
/// <summary>
|
||||
/// Last update time
|
||||
/// </summary>
|
||||
public DateTime LastUpdate { get; set; } = DateTime.Now;
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Name} ({Type}) - {CurrentBrightness}%";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update monitor status
|
||||
/// </summary>
|
||||
public void UpdateStatus(int brightness, bool isAvailable = true)
|
||||
{
|
||||
IsAvailable = isAvailable;
|
||||
if (isAvailable)
|
||||
{
|
||||
CurrentBrightness = brightness;
|
||||
LastUpdate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +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;
|
||||
|
||||
namespace PowerDisplay.Core.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitor control capabilities flags
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum MonitorCapabilities
|
||||
{
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Supports brightness control
|
||||
/// </summary>
|
||||
Brightness = 1 << 0,
|
||||
|
||||
/// <summary>
|
||||
/// Supports contrast control
|
||||
/// </summary>
|
||||
Contrast = 1 << 1,
|
||||
|
||||
/// <summary>
|
||||
/// Supports DDC/CI protocol
|
||||
/// </summary>
|
||||
DdcCi = 1 << 2,
|
||||
|
||||
/// <summary>
|
||||
/// Supports WMI control
|
||||
/// </summary>
|
||||
Wmi = 1 << 3,
|
||||
|
||||
/// <summary>
|
||||
/// Supports HDR
|
||||
/// </summary>
|
||||
Hdr = 1 << 4,
|
||||
|
||||
/// <summary>
|
||||
/// Supports high-level monitor API
|
||||
/// </summary>
|
||||
HighLevel = 1 << 5,
|
||||
|
||||
/// <summary>
|
||||
/// Supports volume control
|
||||
/// </summary>
|
||||
Volume = 1 << 6,
|
||||
}
|
||||
}
|
||||
@@ -1,58 +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;
|
||||
|
||||
namespace PowerDisplay.Core.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitor operation result
|
||||
/// </summary>
|
||||
public readonly struct MonitorOperationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the operation was successful
|
||||
/// </summary>
|
||||
public bool IsSuccess { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message
|
||||
/// </summary>
|
||||
public string? ErrorMessage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// System error code
|
||||
/// </summary>
|
||||
public int? ErrorCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Operation timestamp
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; }
|
||||
|
||||
private MonitorOperationResult(bool isSuccess, string? errorMessage = null, int? errorCode = null)
|
||||
{
|
||||
IsSuccess = isSuccess;
|
||||
ErrorMessage = errorMessage;
|
||||
ErrorCode = errorCode;
|
||||
Timestamp = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a successful result
|
||||
/// </summary>
|
||||
public static MonitorOperationResult Success() => new(true);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a failed result
|
||||
/// </summary>
|
||||
public static MonitorOperationResult Failure(string errorMessage, int? errorCode = null)
|
||||
=> new(false, errorMessage, errorCode);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return IsSuccess ? "Success" : $"Failed: {ErrorMessage}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +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.
|
||||
|
||||
namespace PowerDisplay.Core.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitor type enumeration
|
||||
/// </summary>
|
||||
public enum MonitorType
|
||||
{
|
||||
/// <summary>
|
||||
/// Unknown type
|
||||
/// </summary>
|
||||
Unknown,
|
||||
|
||||
/// <summary>
|
||||
/// Internal display (laptop screen, controlled via WMI)
|
||||
/// </summary>
|
||||
Internal,
|
||||
|
||||
/// <summary>
|
||||
/// External display (controlled via DDC/CI)
|
||||
/// </summary>
|
||||
External,
|
||||
|
||||
/// <summary>
|
||||
/// HDR display (controlled via Display Config API)
|
||||
/// </summary>
|
||||
HDR,
|
||||
}
|
||||
}
|
||||
@@ -1,594 +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.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using PowerDisplay.Core.Interfaces;
|
||||
using PowerDisplay.Core.Models;
|
||||
using PowerDisplay.Native.DDC;
|
||||
using PowerDisplay.Native.WMI;
|
||||
using Monitor = PowerDisplay.Core.Models.Monitor;
|
||||
|
||||
namespace PowerDisplay.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitor manager for unified control of all monitors
|
||||
/// </summary>
|
||||
public class MonitorManager : IMonitorManager, IDisposable
|
||||
{
|
||||
private readonly List<Monitor> _monitors = new();
|
||||
private readonly List<IMonitorController> _controllers = new();
|
||||
private readonly SemaphoreSlim _discoveryLock = new(1, 1);
|
||||
private readonly Timer _refreshTimer;
|
||||
private bool _disposed;
|
||||
|
||||
public IReadOnlyList<Monitor> Monitors => _monitors.AsReadOnly();
|
||||
|
||||
public event EventHandler<MonitorListChangedEventArgs>? MonitorsChanged;
|
||||
|
||||
public event EventHandler<MonitorStatusChangedEventArgs>? MonitorStatusChanged;
|
||||
|
||||
public MonitorManager()
|
||||
{
|
||||
// Initialize controllers
|
||||
InitializeControllers();
|
||||
|
||||
// Set up periodic refresh timer (check every 30 seconds)
|
||||
// Use synchronous callback to avoid async void issues
|
||||
_refreshTimer = new Timer(_ => _ = RefreshMonitorStatusSafeAsync(), null, TimeSpan.Zero, TimeSpan.FromSeconds(30));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safe wrapper for RefreshMonitorStatusAsync to catch exceptions in timer callback
|
||||
/// </summary>
|
||||
private async Task RefreshMonitorStatusSafeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await RefreshMonitorStatusAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Periodic refresh failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize controllers
|
||||
/// </summary>
|
||||
private void InitializeControllers()
|
||||
{
|
||||
try
|
||||
{
|
||||
// DDC/CI controller (external monitors)
|
||||
_controllers.Add(new DdcCiController());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to initialize DDC/CI controller: {ex.Message}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// WMI controller (internal monitors)
|
||||
// First check if WMI is available
|
||||
if (WmiController.IsWmiAvailable())
|
||||
{
|
||||
_controllers.Add(new WmiController());
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInfo("WMI brightness control not available on this system");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to initialize WMI controller: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discover all monitors
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _discoveryLock.WaitAsync(cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
var oldMonitors = _monitors.ToList();
|
||||
var newMonitors = new List<Monitor>();
|
||||
|
||||
// Discover monitors supported by all controllers in parallel
|
||||
var discoveryTasks = _controllers.Select(async controller =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var monitors = await controller.DiscoverMonitorsAsync(cancellationToken);
|
||||
return (Controller: controller, Monitors: monitors.ToList());
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// If a controller fails, return empty list
|
||||
return (Controller: controller, Monitors: new List<Monitor>());
|
||||
}
|
||||
});
|
||||
|
||||
var results = await Task.WhenAll(discoveryTasks);
|
||||
|
||||
// Collect all discovered monitors
|
||||
foreach (var (controller, monitors) in results)
|
||||
{
|
||||
foreach (var monitor in monitors)
|
||||
{
|
||||
// Verify if monitor can be controlled
|
||||
if (await controller.CanControlMonitorAsync(monitor, cancellationToken))
|
||||
{
|
||||
// Get current brightness
|
||||
try
|
||||
{
|
||||
var brightnessInfo = await controller.GetBrightnessAsync(monitor, cancellationToken);
|
||||
if (brightnessInfo.IsValid)
|
||||
{
|
||||
monitor.CurrentBrightness = brightnessInfo.ToPercentage();
|
||||
monitor.MinBrightness = brightnessInfo.Minimum;
|
||||
monitor.MaxBrightness = brightnessInfo.Maximum;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If unable to get brightness, use default values
|
||||
}
|
||||
|
||||
newMonitors.Add(monitor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update monitor list
|
||||
_monitors.Clear();
|
||||
_monitors.AddRange(newMonitors);
|
||||
|
||||
// Trigger change events
|
||||
var addedMonitors = newMonitors.Where(m => !oldMonitors.Any(o => o.Id == m.Id)).ToList();
|
||||
var removedMonitors = oldMonitors.Where(o => !newMonitors.Any(m => m.Id == o.Id)).ToList();
|
||||
|
||||
if (addedMonitors.Count > 0 || removedMonitors.Count > 0)
|
||||
{
|
||||
MonitorsChanged?.Invoke(this, new MonitorListChangedEventArgs(
|
||||
addedMonitors.AsReadOnly(),
|
||||
removedMonitors.AsReadOnly(),
|
||||
_monitors.AsReadOnly()));
|
||||
}
|
||||
|
||||
return _monitors.AsReadOnly();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_discoveryLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get brightness of the specified monitor
|
||||
/// </summary>
|
||||
public async Task<BrightnessInfo> GetBrightnessAsync(string monitorId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var monitor = GetMonitor(monitorId);
|
||||
if (monitor == null)
|
||||
{
|
||||
return BrightnessInfo.Invalid;
|
||||
}
|
||||
|
||||
var controller = GetControllerForMonitor(monitor);
|
||||
if (controller == null)
|
||||
{
|
||||
return BrightnessInfo.Invalid;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var brightnessInfo = await controller.GetBrightnessAsync(monitor, cancellationToken);
|
||||
|
||||
// Update cached brightness value
|
||||
if (brightnessInfo.IsValid)
|
||||
{
|
||||
var oldBrightness = monitor.CurrentBrightness;
|
||||
monitor.UpdateStatus(brightnessInfo.ToPercentage(), true);
|
||||
|
||||
// Trigger status change event
|
||||
if (oldBrightness != monitor.CurrentBrightness)
|
||||
{
|
||||
MonitorStatusChanged?.Invoke(this, new MonitorStatusChangedEventArgs(
|
||||
monitor, oldBrightness, monitor.CurrentBrightness, true, true));
|
||||
}
|
||||
}
|
||||
|
||||
return brightnessInfo;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Mark monitor as unavailable
|
||||
monitor.IsAvailable = false;
|
||||
return BrightnessInfo.Invalid;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set brightness of the specified monitor
|
||||
/// </summary>
|
||||
public async Task<MonitorOperationResult> SetBrightnessAsync(string monitorId, int brightness, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var monitor = GetMonitor(monitorId);
|
||||
if (monitor == null)
|
||||
{
|
||||
return MonitorOperationResult.Failure("Monitor not found");
|
||||
}
|
||||
|
||||
var controller = GetControllerForMonitor(monitor);
|
||||
if (controller == null)
|
||||
{
|
||||
return MonitorOperationResult.Failure("No controller available for this monitor");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var oldBrightness = monitor.CurrentBrightness;
|
||||
var result = await controller.SetBrightnessAsync(monitor, brightness, cancellationToken);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
// Update monitor status
|
||||
monitor.UpdateStatus(brightness, true);
|
||||
|
||||
// Trigger status change event
|
||||
MonitorStatusChanged?.Invoke(this, new MonitorStatusChangedEventArgs(
|
||||
monitor, oldBrightness, brightness, true, true));
|
||||
}
|
||||
else
|
||||
{
|
||||
// If setting fails, monitor may be unavailable
|
||||
monitor.IsAvailable = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
monitor.IsAvailable = false;
|
||||
return MonitorOperationResult.Failure($"Exception setting brightness: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set brightness of all monitors
|
||||
/// </summary>
|
||||
public async Task<IEnumerable<MonitorOperationResult>> SetAllBrightnessAsync(int brightness, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tasks = _monitors
|
||||
.Where(m => m.IsAvailable)
|
||||
.Select(async monitor =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return await SetBrightnessAsync(monitor.Id, brightness, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return MonitorOperationResult.Failure($"Failed to set brightness for {monitor.Name}: {ex.Message}");
|
||||
}
|
||||
});
|
||||
|
||||
return await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set contrast of the specified monitor
|
||||
/// </summary>
|
||||
public async Task<MonitorOperationResult> SetContrastAsync(string monitorId, int contrast, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var monitor = GetMonitor(monitorId);
|
||||
if (monitor == null)
|
||||
{
|
||||
return MonitorOperationResult.Failure("Monitor not found");
|
||||
}
|
||||
|
||||
var controller = GetControllerForMonitor(monitor) as IExtendedMonitorController;
|
||||
if (controller == null)
|
||||
{
|
||||
return MonitorOperationResult.Failure("No extended controller available for this monitor");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var oldContrast = monitor.CurrentContrast;
|
||||
var result = await controller.SetContrastAsync(monitor, contrast, cancellationToken);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
monitor.CurrentContrast = contrast;
|
||||
monitor.LastUpdate = DateTime.Now;
|
||||
}
|
||||
else
|
||||
{
|
||||
monitor.IsAvailable = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return MonitorOperationResult.Failure($"Exception setting contrast: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set volume of the specified monitor
|
||||
/// </summary>
|
||||
public async Task<MonitorOperationResult> SetVolumeAsync(string monitorId, int volume, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var monitor = GetMonitor(monitorId);
|
||||
if (monitor == null)
|
||||
{
|
||||
return MonitorOperationResult.Failure("Monitor not found");
|
||||
}
|
||||
|
||||
var controller = GetControllerForMonitor(monitor) as IExtendedMonitorController;
|
||||
if (controller == null)
|
||||
{
|
||||
return MonitorOperationResult.Failure("No extended controller available for this monitor");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var oldVolume = monitor.CurrentVolume;
|
||||
var result = await controller.SetVolumeAsync(monitor, volume, cancellationToken);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
monitor.CurrentVolume = volume;
|
||||
monitor.LastUpdate = DateTime.Now;
|
||||
}
|
||||
else
|
||||
{
|
||||
monitor.IsAvailable = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return MonitorOperationResult.Failure($"Exception setting volume: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get monitor color temperature
|
||||
/// </summary>
|
||||
public async Task<BrightnessInfo> GetColorTemperatureAsync(string monitorId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var monitor = GetMonitor(monitorId);
|
||||
if (monitor == null)
|
||||
{
|
||||
return BrightnessInfo.Invalid;
|
||||
}
|
||||
|
||||
var controller = GetControllerForMonitor(monitor) as DdcCiController;
|
||||
if (controller == null)
|
||||
{
|
||||
return BrightnessInfo.Invalid;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await controller.GetColorTemperatureAsync(monitor, cancellationToken);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return BrightnessInfo.Invalid;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set monitor color temperature
|
||||
/// </summary>
|
||||
public async Task<MonitorOperationResult> SetColorTemperatureAsync(string monitorId, int colorTemperature, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var monitor = GetMonitor(monitorId);
|
||||
if (monitor == null)
|
||||
{
|
||||
return MonitorOperationResult.Failure("Monitor not found");
|
||||
}
|
||||
|
||||
var controller = GetControllerForMonitor(monitor) as DdcCiController;
|
||||
if (controller == null)
|
||||
{
|
||||
return MonitorOperationResult.Failure("DDC/CI controller not available for this monitor");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var oldTemperature = monitor.CurrentColorTemperature;
|
||||
var result = await controller.SetColorTemperatureAsync(monitor, colorTemperature, cancellationToken);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
monitor.CurrentColorTemperature = colorTemperature;
|
||||
monitor.LastUpdate = DateTime.Now;
|
||||
|
||||
// Trigger status change event
|
||||
MonitorStatusChanged?.Invoke(this, new MonitorStatusChangedEventArgs(
|
||||
monitor,
|
||||
$"Color temperature changed from {oldTemperature}K to {colorTemperature}K",
|
||||
MonitorStatusChangedEventArgs.ChangeType.ColorTemperature
|
||||
));
|
||||
}
|
||||
else
|
||||
{
|
||||
monitor.IsAvailable = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return MonitorOperationResult.Failure($"Exception setting color temperature: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize color temperature for a monitor (async operation)
|
||||
/// </summary>
|
||||
public async Task InitializeColorTemperatureAsync(string monitorId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tempInfo = await GetColorTemperatureAsync(monitorId, cancellationToken);
|
||||
if (tempInfo.IsValid)
|
||||
{
|
||||
var monitor = GetMonitor(monitorId);
|
||||
if (monitor != null)
|
||||
{
|
||||
// Convert VCP value to approximate Kelvin temperature
|
||||
// This is a rough mapping - actual values depend on monitor implementation
|
||||
var kelvin = ConvertVcpValueToKelvin(tempInfo.Current, tempInfo.Maximum);
|
||||
monitor.CurrentColorTemperature = kelvin;
|
||||
|
||||
Logger.LogInfo($"Initialized color temperature for {monitorId}: {kelvin}K (VCP: {tempInfo.Current}/{tempInfo.Maximum})");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to initialize color temperature for {monitorId}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert VCP value to approximate Kelvin temperature
|
||||
/// </summary>
|
||||
private static int ConvertVcpValueToKelvin(int vcpValue, int maxVcpValue)
|
||||
{
|
||||
// Standard color temperature range mapping
|
||||
const int minKelvin = 2000; // Warm
|
||||
const int maxKelvin = 10000; // Cool
|
||||
|
||||
// Normalize VCP value to 0-1 range
|
||||
double normalizedVcp = maxVcpValue > 0 ? (double)vcpValue / maxVcpValue : 0.5;
|
||||
|
||||
// Map to Kelvin range
|
||||
int kelvin = (int)(minKelvin + (normalizedVcp * (maxKelvin - minKelvin)));
|
||||
|
||||
return Math.Clamp(kelvin, minKelvin, maxKelvin);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refresh monitor status
|
||||
/// </summary>
|
||||
public async Task RefreshMonitorStatusAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tasks = _monitors.Select(async monitor =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var controller = GetControllerForMonitor(monitor);
|
||||
if (controller == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate connection status
|
||||
var isConnected = await controller.ValidateConnectionAsync(monitor, cancellationToken);
|
||||
var oldAvailability = monitor.IsAvailable;
|
||||
|
||||
if (isConnected)
|
||||
{
|
||||
// 获取当前亮度
|
||||
var brightnessInfo = await controller.GetBrightnessAsync(monitor, cancellationToken);
|
||||
if (brightnessInfo.IsValid)
|
||||
{
|
||||
var oldBrightness = monitor.CurrentBrightness;
|
||||
monitor.UpdateStatus(brightnessInfo.ToPercentage(), true);
|
||||
|
||||
// Trigger status change event
|
||||
if (oldBrightness != monitor.CurrentBrightness || oldAvailability != monitor.IsAvailable)
|
||||
{
|
||||
MonitorStatusChanged?.Invoke(this, new MonitorStatusChangedEventArgs(
|
||||
monitor, oldBrightness, monitor.CurrentBrightness, oldAvailability, monitor.IsAvailable));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
monitor.IsAvailable = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
monitor.IsAvailable = false;
|
||||
|
||||
// Trigger availability change event
|
||||
if (oldAvailability != monitor.IsAvailable)
|
||||
{
|
||||
MonitorStatusChanged?.Invoke(this, new MonitorStatusChangedEventArgs(
|
||||
monitor, monitor.CurrentBrightness, monitor.CurrentBrightness, oldAvailability, monitor.IsAvailable));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Refresh failed, mark as unavailable
|
||||
monitor.IsAvailable = false;
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get monitor by ID
|
||||
/// </summary>
|
||||
public Monitor? GetMonitor(string monitorId)
|
||||
{
|
||||
return _monitors.FirstOrDefault(m => m.Id == monitorId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get controller for the monitor
|
||||
/// </summary>
|
||||
private IMonitorController? GetControllerForMonitor(Monitor monitor)
|
||||
{
|
||||
return _controllers.FirstOrDefault(c => c.SupportedType == monitor.Type);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed && disposing)
|
||||
{
|
||||
_refreshTimer?.Dispose();
|
||||
_discoveryLock?.Dispose();
|
||||
|
||||
// Release all controllers
|
||||
foreach (var controller in _controllers)
|
||||
{
|
||||
controller?.Dispose();
|
||||
}
|
||||
|
||||
_controllers.Clear();
|
||||
_monitors.Clear();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
|
||||
namespace PowerDisplay.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Simple async property updater - UI updates immediately, hardware updates use latest value.
|
||||
/// When hardware operation completes, immediately applies the latest queued value if changed.
|
||||
/// No debounce delay - just serial execution with latest value.
|
||||
/// </summary>
|
||||
public class MonitorPropertyManager : IDisposable
|
||||
{
|
||||
private readonly SemaphoreSlim _operationSemaphore = new(1, 1);
|
||||
private readonly string _monitorId;
|
||||
private readonly string _propertyName;
|
||||
private readonly object _stateLock = new object();
|
||||
|
||||
private int _currentValue = -1; // Value currently applied to hardware
|
||||
private int _targetValue = -1; // Latest value user wants
|
||||
private bool _isRunning = false; // Is update task running
|
||||
|
||||
public MonitorPropertyManager(string monitorId, string propertyName)
|
||||
{
|
||||
_monitorId = monitorId;
|
||||
_propertyName = propertyName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue a property update - UI thread friendly (non-blocking)
|
||||
/// </summary>
|
||||
public void QueueUpdate(int newValue, Func<int, CancellationToken, Task<bool>> updateAction)
|
||||
{
|
||||
bool shouldStartTask = false;
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
_targetValue = newValue;
|
||||
|
||||
// Only start new task if no task is currently running
|
||||
if (!_isRunning)
|
||||
{
|
||||
_isRunning = true;
|
||||
shouldStartTask = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Start update task if needed (outside lock)
|
||||
if (shouldStartTask)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await _operationSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
await ExecuteUpdatesAsync(updateAction);
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
_isRunning = false;
|
||||
}
|
||||
_operationSemaphore.Release();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute updates until target value matches current value
|
||||
/// No debounce delay - immediately applies the latest target value after current operation completes
|
||||
/// </summary>
|
||||
private async Task ExecuteUpdatesAsync(Func<int, CancellationToken, Task<bool>> updateAction)
|
||||
{
|
||||
int consecutiveFailures = 0;
|
||||
const int MaxConsecutiveFailures = 3; // 最多连续失败3次就放弃
|
||||
|
||||
while (true)
|
||||
{
|
||||
int valueToApply;
|
||||
|
||||
// Check if there's a new value to apply
|
||||
lock (_stateLock)
|
||||
{
|
||||
if (_targetValue == _currentValue)
|
||||
{
|
||||
// Target matches current, no update needed
|
||||
return;
|
||||
}
|
||||
|
||||
valueToApply = _targetValue;
|
||||
}
|
||||
|
||||
// Execute hardware update (outside lock)
|
||||
try
|
||||
{
|
||||
ManagedCommon.Logger.LogDebug($"[{_monitorId}] {_propertyName} applying value {valueToApply}");
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
||||
bool success = await updateAction(valueToApply, cts.Token);
|
||||
|
||||
if (success)
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
_currentValue = valueToApply;
|
||||
}
|
||||
ManagedCommon.Logger.LogDebug($"[{_monitorId}] {_propertyName} successfully updated to {valueToApply}");
|
||||
consecutiveFailures = 0; // 成功后重置失败计数
|
||||
}
|
||||
else
|
||||
{
|
||||
consecutiveFailures++;
|
||||
ManagedCommon.Logger.LogWarning($"[{_monitorId}] {_propertyName} failed to update to {valueToApply} (attempt {consecutiveFailures}/{MaxConsecutiveFailures})");
|
||||
|
||||
if (consecutiveFailures >= MaxConsecutiveFailures)
|
||||
{
|
||||
ManagedCommon.Logger.LogError($"[{_monitorId}] {_propertyName} failed {MaxConsecutiveFailures} times, giving up. Hardware may not support this feature.");
|
||||
// 放弃:假装成功,避免无限循环
|
||||
lock (_stateLock)
|
||||
{
|
||||
_currentValue = valueToApply;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
consecutiveFailures++;
|
||||
ManagedCommon.Logger.LogError($"[{_monitorId}] {_propertyName} update exception: {ex.Message} (attempt {consecutiveFailures}/{MaxConsecutiveFailures})");
|
||||
|
||||
if (consecutiveFailures >= MaxConsecutiveFailures)
|
||||
{
|
||||
ManagedCommon.Logger.LogError($"[{_monitorId}] {_propertyName} exception {MaxConsecutiveFailures} times, giving up. Hardware may not support this feature.");
|
||||
// 放弃:假装成功,避免无限循环
|
||||
lock (_stateLock)
|
||||
{
|
||||
_currentValue = valueToApply;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Loop back to check if target changed during the update
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait for all pending updates to complete
|
||||
/// </summary>
|
||||
public async Task FlushAsync()
|
||||
{
|
||||
// Wait for operation semaphore to ensure all updates completed
|
||||
await _operationSemaphore.WaitAsync();
|
||||
_operationSemaphore.Release();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_operationSemaphore?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,343 +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.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
|
||||
namespace PowerDisplay.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages monitor parameter state in a separate file from main settings.
|
||||
/// This avoids FileSystemWatcher feedback loops by separating read-only config (settings.json)
|
||||
/// from frequently-updated state (monitor_state.json).
|
||||
/// </summary>
|
||||
public class MonitorStateManager : IDisposable
|
||||
{
|
||||
private const int SaveIntervalMs = 2000; // Check every 2 seconds
|
||||
|
||||
private readonly Timer _saveTimer;
|
||||
private readonly string _stateFilePath;
|
||||
private readonly ConcurrentDictionary<string, MonitorParameters> _parameters = new();
|
||||
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new() { WriteIndented = true };
|
||||
|
||||
private volatile bool _isDirty = false;
|
||||
private bool _disposed;
|
||||
|
||||
// Simple mutual exclusion for save operations (0 = idle, 1 = saving)
|
||||
private int _isSaving = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Monitor parameters with thread-safe volatile properties
|
||||
/// </summary>
|
||||
private sealed class MonitorParameters
|
||||
{
|
||||
private int _brightness;
|
||||
private int _colorTemperature;
|
||||
private int _contrast;
|
||||
private int _volume;
|
||||
|
||||
public int Brightness
|
||||
{
|
||||
get => Volatile.Read(ref _brightness);
|
||||
set => Volatile.Write(ref _brightness, value);
|
||||
}
|
||||
|
||||
public int ColorTemperature
|
||||
{
|
||||
get => Volatile.Read(ref _colorTemperature);
|
||||
set => Volatile.Write(ref _colorTemperature, value);
|
||||
}
|
||||
|
||||
public int Contrast
|
||||
{
|
||||
get => Volatile.Read(ref _contrast);
|
||||
set => Volatile.Write(ref _contrast, value);
|
||||
}
|
||||
|
||||
public int Volume
|
||||
{
|
||||
get => Volatile.Read(ref _volume);
|
||||
set => Volatile.Write(ref _volume, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializable state for JSON persistence
|
||||
/// Dictionary key should be HardwareId (e.g., "GSM5C6D") for stable identification
|
||||
/// Legacy files may have used InternalName (e.g., "DISPLAY1_0_3") which will still load but won't match after reconnection
|
||||
/// </summary>
|
||||
private sealed class MonitorStateFile
|
||||
{
|
||||
public Dictionary<string, MonitorStateEntry> Monitors { get; set; } = new();
|
||||
public DateTime LastUpdated { get; set; }
|
||||
}
|
||||
|
||||
private sealed class MonitorStateEntry
|
||||
{
|
||||
public int Brightness { get; set; }
|
||||
public int ColorTemperature { get; set; }
|
||||
public int Contrast { get; set; }
|
||||
public int Volume { get; set; }
|
||||
public DateTime LastUpdated { get; set; }
|
||||
}
|
||||
|
||||
public MonitorStateManager()
|
||||
{
|
||||
// Store state file in same location as settings.json but with different name
|
||||
var settingsPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
var powerToysPath = Path.Combine(settingsPath, "Microsoft", "PowerToys", "PowerDisplay");
|
||||
|
||||
if (!Directory.Exists(powerToysPath))
|
||||
{
|
||||
Directory.CreateDirectory(powerToysPath);
|
||||
}
|
||||
|
||||
_stateFilePath = Path.Combine(powerToysPath, "monitor_state.json");
|
||||
|
||||
// Load existing state if available
|
||||
LoadStateFromDisk();
|
||||
|
||||
// Start periodic timer that checks every 2 seconds
|
||||
_saveTimer = new Timer(
|
||||
_ => SaveStateIfDirty(),
|
||||
null,
|
||||
TimeSpan.FromSeconds(SaveIntervalMs / 1000.0),
|
||||
TimeSpan.FromSeconds(SaveIntervalMs / 1000.0)
|
||||
);
|
||||
|
||||
Logger.LogInfo($"MonitorStateManager initialized, state file: {_stateFilePath}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update monitor parameter in memory (lock-free, non-blocking)
|
||||
/// Uses HardwareId as the stable key
|
||||
/// </summary>
|
||||
public void UpdateMonitorParameter(string hardwareId, string property, int value)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(hardwareId))
|
||||
{
|
||||
Logger.LogWarning($"Cannot update monitor parameter: HardwareId is empty");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get or create parameter entry using HardwareId
|
||||
var parameters = _parameters.GetOrAdd(hardwareId, _ => new MonitorParameters());
|
||||
|
||||
// Update the specific property (volatile write)
|
||||
switch (property)
|
||||
{
|
||||
case "Brightness":
|
||||
parameters.Brightness = value;
|
||||
break;
|
||||
case "ColorTemperature":
|
||||
parameters.ColorTemperature = value;
|
||||
break;
|
||||
case "Contrast":
|
||||
parameters.Contrast = value;
|
||||
break;
|
||||
case "Volume":
|
||||
parameters.Volume = value;
|
||||
break;
|
||||
default:
|
||||
Logger.LogWarning($"Unknown property: {property}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark as dirty (will be saved in next timer cycle)
|
||||
_isDirty = true;
|
||||
|
||||
Logger.LogTrace($"[State] Updated {property}={value} for monitor HardwareId='{hardwareId}'");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to update monitor parameter: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get saved parameters for a monitor using HardwareId
|
||||
/// </summary>
|
||||
public (int Brightness, int ColorTemperature, int Contrast, int Volume)? GetMonitorParameters(string hardwareId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(hardwareId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_parameters.TryGetValue(hardwareId, out var parameters))
|
||||
{
|
||||
return (parameters.Brightness, parameters.ColorTemperature, parameters.Contrast, parameters.Volume);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if state exists for a monitor (by HardwareId)
|
||||
/// </summary>
|
||||
public bool HasMonitorState(string hardwareId)
|
||||
{
|
||||
return !string.IsNullOrEmpty(hardwareId) && _parameters.ContainsKey(hardwareId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load state from disk
|
||||
/// </summary>
|
||||
private void LoadStateFromDisk()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_stateFilePath))
|
||||
{
|
||||
Logger.LogInfo("[State] No existing state file found, starting fresh");
|
||||
return;
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(_stateFilePath);
|
||||
var state = JsonSerializer.Deserialize<MonitorStateFile>(json);
|
||||
|
||||
if (state?.Monitors != null)
|
||||
{
|
||||
foreach (var kvp in state.Monitors)
|
||||
{
|
||||
var monitorKey = kvp.Key; // Should be HardwareId (e.g., "GSM5C6D")
|
||||
var entry = kvp.Value;
|
||||
|
||||
var parameters = _parameters.GetOrAdd(monitorKey, _ => new MonitorParameters());
|
||||
parameters.Brightness = entry.Brightness;
|
||||
parameters.ColorTemperature = entry.ColorTemperature;
|
||||
parameters.Contrast = entry.Contrast;
|
||||
parameters.Volume = entry.Volume;
|
||||
}
|
||||
|
||||
Logger.LogInfo($"[State] Loaded state for {state.Monitors.Count} monitors from {_stateFilePath}");
|
||||
Logger.LogInfo($"[State] Monitor keys in state file: {string.Join(", ", state.Monitors.Keys)}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to load monitor state: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Periodic save check - saves state if dirty
|
||||
/// </summary>
|
||||
private async void SaveStateIfDirty()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Quick check without lock
|
||||
if (!_isDirty || _disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to acquire save permission (non-blocking)
|
||||
if (Interlocked.CompareExchange(ref _isSaving, 1, 0) != 0)
|
||||
{
|
||||
// Already saving, skip this cycle
|
||||
return;
|
||||
}
|
||||
|
||||
// Double check
|
||||
if (!_isDirty)
|
||||
{
|
||||
Interlocked.Exchange(ref _isSaving, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build state file
|
||||
var state = new MonitorStateFile
|
||||
{
|
||||
LastUpdated = DateTime.Now
|
||||
};
|
||||
|
||||
var now = DateTime.Now;
|
||||
foreach (var kvp in _parameters)
|
||||
{
|
||||
var monitorId = kvp.Key;
|
||||
var parameters = kvp.Value;
|
||||
|
||||
state.Monitors[monitorId] = new MonitorStateEntry
|
||||
{
|
||||
Brightness = parameters.Brightness, // Volatile read
|
||||
ColorTemperature = parameters.ColorTemperature,
|
||||
Contrast = parameters.Contrast,
|
||||
Volume = parameters.Volume,
|
||||
LastUpdated = now
|
||||
};
|
||||
}
|
||||
|
||||
// Write to disk
|
||||
var json = JsonSerializer.Serialize(state, _jsonOptions);
|
||||
await File.WriteAllTextAsync(_stateFilePath, json);
|
||||
|
||||
// Clear dirty flag
|
||||
_isDirty = false;
|
||||
|
||||
Logger.LogInfo($"[State] Saved state for {state.Monitors.Count} monitors");
|
||||
|
||||
// Release save permission
|
||||
Interlocked.Exchange(ref _isSaving, 0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to save monitor state: {ex.Message}");
|
||||
// Release save permission even on error
|
||||
Interlocked.Exchange(ref _isSaving, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flush pending changes immediately (for program exit)
|
||||
/// </summary>
|
||||
public async Task FlushAsync()
|
||||
{
|
||||
if (_isDirty && !_disposed)
|
||||
{
|
||||
// Wait for any ongoing save to complete
|
||||
while (Volatile.Read(ref _isSaving) == 1)
|
||||
{
|
||||
await Task.Delay(50);
|
||||
}
|
||||
|
||||
// Trigger save
|
||||
SaveStateIfDirty();
|
||||
|
||||
// Wait for save to complete
|
||||
while (Volatile.Read(ref _isSaving) == 1)
|
||||
{
|
||||
await Task.Delay(50);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
|
||||
// Stop timer
|
||||
_saveTimer?.Dispose();
|
||||
|
||||
// Flush any pending changes
|
||||
FlushAsync().GetAwaiter().GetResult();
|
||||
|
||||
Logger.LogInfo("MonitorStateManager disposed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 Microsoft.Windows.ApplicationModel.Resources;
|
||||
|
||||
namespace PowerDisplay.Helpers
|
||||
{
|
||||
public static class ResourceLoaderInstance
|
||||
{
|
||||
public static ResourceLoader ResourceLoader { get; private set; }
|
||||
|
||||
static ResourceLoaderInstance()
|
||||
{
|
||||
ResourceLoader = new ResourceLoader("PowerToys.PowerDisplay.pri");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,206 +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.IO;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace PowerDisplay.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 管理应用程序主题设置
|
||||
/// </summary>
|
||||
public static class ThemeManager
|
||||
{
|
||||
private const string ThemeSettingKey = "AppTheme";
|
||||
private static readonly string SettingsFilePath;
|
||||
private static readonly ISettingsUtils _settingsUtils = new SettingsUtils();
|
||||
|
||||
static ThemeManager()
|
||||
{
|
||||
// 使用本地AppData文件夹存储设置
|
||||
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
var appFolder = Path.Combine(localAppData, "PowerDisplay");
|
||||
|
||||
// 确保文件夹存在
|
||||
if (!Directory.Exists(appFolder))
|
||||
{
|
||||
Directory.CreateDirectory(appFolder);
|
||||
}
|
||||
|
||||
SettingsFilePath = Path.Combine(appFolder, "theme.settings");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取保存的主题设置
|
||||
/// </summary>
|
||||
public static ElementTheme GetSavedTheme()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(SettingsFilePath))
|
||||
{
|
||||
var savedTheme = File.ReadAllText(SettingsFilePath);
|
||||
return savedTheme switch
|
||||
{
|
||||
"Light" => ElementTheme.Light,
|
||||
"Dark" => ElementTheme.Dark,
|
||||
_ => ElementTheme.Default
|
||||
};
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 如果读取失败,返回默认值
|
||||
}
|
||||
|
||||
return ElementTheme.Default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存主题设置
|
||||
/// </summary>
|
||||
public static void SaveTheme(ElementTheme theme)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllText(SettingsFilePath, theme.ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略保存错误
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用主题到窗口
|
||||
/// </summary>
|
||||
public static void ApplyTheme(Window window, ElementTheme theme)
|
||||
{
|
||||
if (window?.Content is FrameworkElement rootElement)
|
||||
{
|
||||
rootElement.RequestedTheme = theme;
|
||||
SaveTheme(theme);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换主题(深色/浅色)
|
||||
/// </summary>
|
||||
public static ElementTheme ToggleTheme(Window window)
|
||||
{
|
||||
var currentTheme = GetCurrentTheme(window);
|
||||
var newTheme = currentTheme == ElementTheme.Light ? ElementTheme.Dark : ElementTheme.Light;
|
||||
|
||||
ApplyTheme(window, newTheme);
|
||||
return newTheme;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前窗口的主题
|
||||
/// </summary>
|
||||
public static ElementTheme GetCurrentTheme(Window window)
|
||||
{
|
||||
if (window?.Content is FrameworkElement rootElement)
|
||||
{
|
||||
return rootElement.RequestedTheme switch
|
||||
{
|
||||
ElementTheme.Light => ElementTheme.Light,
|
||||
ElementTheme.Dark => ElementTheme.Dark,
|
||||
_ => Application.Current.RequestedTheme == ApplicationTheme.Light
|
||||
? ElementTheme.Light
|
||||
: ElementTheme.Dark
|
||||
};
|
||||
}
|
||||
|
||||
return ElementTheme.Default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否为深色主题
|
||||
/// </summary>
|
||||
public static bool IsDarkTheme(Window window)
|
||||
{
|
||||
return GetCurrentTheme(window) == ElementTheme.Dark;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从PowerToys设置中获取主题
|
||||
/// </summary>
|
||||
public static ElementTheme GetThemeFromPowerToysSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>("PowerDisplay");
|
||||
return settings.Properties.Theme switch
|
||||
{
|
||||
"Light" => ElementTheme.Light,
|
||||
"Dark" => ElementTheme.Dark,
|
||||
_ => ElementTheme.Default
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
return ElementTheme.Default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将主题保存到PowerToys设置
|
||||
/// </summary>
|
||||
public static void SaveThemeToPowerToysSettings(ElementTheme theme)
|
||||
{
|
||||
try
|
||||
{
|
||||
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>("PowerDisplay");
|
||||
settings.Properties.Theme = theme.ToString();
|
||||
_settingsUtils.SaveSettings(settings.ToJsonString(), "PowerDisplay");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 记录错误但不阻止操作
|
||||
try
|
||||
{
|
||||
Logger.LogError($"Failed to save theme to PowerToys settings: {ex.Message}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略日志错误
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取保存的主题设置(优先从PowerToys设置读取)
|
||||
/// </summary>
|
||||
public static ElementTheme GetSavedThemeWithPriority()
|
||||
{
|
||||
// 首先尝试从PowerToys设置读取
|
||||
var powerToysTheme = GetThemeFromPowerToysSettings();
|
||||
if (powerToysTheme != ElementTheme.Default)
|
||||
{
|
||||
// 同步到本地设置
|
||||
SaveTheme(powerToysTheme);
|
||||
return powerToysTheme;
|
||||
}
|
||||
|
||||
// 如果PowerToys设置没有或失败,回退到本地设置
|
||||
return GetSavedTheme();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用主题并同步到两个设置系统
|
||||
/// </summary>
|
||||
public static void ApplyThemeAndSync(Window window, ElementTheme theme)
|
||||
{
|
||||
// 应用到窗口
|
||||
ApplyTheme(window, theme);
|
||||
|
||||
// 同步到PowerToys设置
|
||||
SaveThemeToPowerToysSettings(theme);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user