mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-12 07:16:44 +01:00
merging main into branch
This commit is contained in:
6
.github/actions/spell-check/allow/names.txt
vendored
6
.github/actions/spell-check/allow/names.txt
vendored
@@ -56,7 +56,6 @@ damienleroy
|
||||
davidegiacometti
|
||||
debian
|
||||
Deibisu
|
||||
Deibisu
|
||||
Delimarsky
|
||||
Deondre
|
||||
DHowett
|
||||
@@ -87,7 +86,6 @@ jefflord
|
||||
Jordi
|
||||
jyuwono
|
||||
Kairu
|
||||
Kairu
|
||||
Kamra
|
||||
Kantarci
|
||||
Karthick
|
||||
@@ -123,7 +121,6 @@ Quriz
|
||||
randyrants
|
||||
ricardosantos
|
||||
riri
|
||||
riri
|
||||
ritchielawrence
|
||||
robmikh
|
||||
Rutkas
|
||||
@@ -147,6 +144,7 @@ TBM
|
||||
tilovell
|
||||
Triet
|
||||
waaverecords
|
||||
Whuihuan
|
||||
Xpg
|
||||
ycv
|
||||
Yuniardi
|
||||
@@ -157,6 +155,8 @@ Zykova
|
||||
|
||||
# OTHERS
|
||||
|
||||
Bilibili
|
||||
BVID
|
||||
cmdow
|
||||
Controlz
|
||||
cortana
|
||||
|
||||
33
.github/actions/spell-check/expect.txt
vendored
33
.github/actions/spell-check/expect.txt
vendored
@@ -69,11 +69,9 @@ AQS
|
||||
ARandom
|
||||
ARCHITEW
|
||||
ARemapped
|
||||
ari
|
||||
ARPINSTALLLOCATION
|
||||
ARPPRODUCTICON
|
||||
ARRAYSIZE
|
||||
arw
|
||||
asf
|
||||
AShortcut
|
||||
ASingle
|
||||
@@ -151,6 +149,7 @@ CALG
|
||||
callbackptr
|
||||
calpwstr
|
||||
Cangjie
|
||||
caniuse
|
||||
CANRENAME
|
||||
CAPTUREBLT
|
||||
CAPTURECHANGED
|
||||
@@ -259,7 +258,7 @@ CRH
|
||||
critsec
|
||||
Crossdevice
|
||||
CRSEL
|
||||
crw
|
||||
crx
|
||||
CSearch
|
||||
CSettings
|
||||
cso
|
||||
@@ -301,8 +300,8 @@ DCOM
|
||||
dcommon
|
||||
dcomp
|
||||
DComposition
|
||||
dcr
|
||||
dcs
|
||||
DCR
|
||||
DCs
|
||||
ddd
|
||||
DDEIf
|
||||
DDevice
|
||||
@@ -372,7 +371,6 @@ DRAWCLIPBOARD
|
||||
DRAWFRAME
|
||||
drawingcolor
|
||||
dreamsofameaningfullife
|
||||
drf
|
||||
drivedetectionwarning
|
||||
dshow
|
||||
DSTINVERT
|
||||
@@ -416,7 +414,6 @@ editkeyboardwindow
|
||||
EDITSHORTCUTS
|
||||
editshortcutswindow
|
||||
EFile
|
||||
eip
|
||||
ekus
|
||||
emmintrin
|
||||
Emoji
|
||||
@@ -590,7 +587,7 @@ Hiberboot
|
||||
HIBYTE
|
||||
hicon
|
||||
HIDEWINDOW
|
||||
hif
|
||||
Hif
|
||||
HIMAGELIST
|
||||
himl
|
||||
hinst
|
||||
@@ -639,6 +636,7 @@ HWNDLAST
|
||||
HWNDNEXT
|
||||
HWNDPREV
|
||||
hyjiacan
|
||||
IApp
|
||||
IBeam
|
||||
ICapture
|
||||
IClass
|
||||
@@ -665,7 +663,6 @@ IGNOREUNKNOWN
|
||||
IGraphics
|
||||
iid
|
||||
Iindex
|
||||
iiq
|
||||
IJson
|
||||
Ijwhost
|
||||
IKs
|
||||
@@ -730,15 +727,14 @@ ith
|
||||
ITHUMBNAIL
|
||||
IUI
|
||||
IUnknown
|
||||
IUse
|
||||
IWbem
|
||||
IWeb
|
||||
IWIC
|
||||
iwr
|
||||
IYUV
|
||||
jfi
|
||||
jfif
|
||||
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
|
||||
jif
|
||||
jjw
|
||||
jobject
|
||||
jpe
|
||||
@@ -747,7 +743,6 @@ Jsons
|
||||
jsonval
|
||||
junja
|
||||
jxr
|
||||
kdc
|
||||
keybd
|
||||
KEYBDDATA
|
||||
KEYBDINPUT
|
||||
@@ -872,7 +867,6 @@ MAXIMIZEBOX
|
||||
MAXSHORTCUTSIZE
|
||||
maxversiontested
|
||||
MBR
|
||||
mdc
|
||||
MDICHILD
|
||||
MDL
|
||||
mdtext
|
||||
@@ -880,7 +874,6 @@ mdtxt
|
||||
mdwn
|
||||
MEDIASUBTYPE
|
||||
mediatype
|
||||
mef
|
||||
MENUITEMINFO
|
||||
MENUITEMINFOW
|
||||
MERGECOPY
|
||||
@@ -938,7 +931,6 @@ mpmc
|
||||
MRM
|
||||
MRT
|
||||
mru
|
||||
mrw
|
||||
msc
|
||||
mscorlib
|
||||
msdata
|
||||
@@ -1061,7 +1053,6 @@ NOZORDER
|
||||
NPH
|
||||
npmjs
|
||||
NResize
|
||||
nrw
|
||||
nsunt
|
||||
NTAPI
|
||||
ntdll
|
||||
@@ -1091,7 +1082,6 @@ opensource
|
||||
openxmlformats
|
||||
OPTIMIZEFORINVOKE
|
||||
ORAW
|
||||
ori
|
||||
ORPHANEDDIALOGTITLE
|
||||
ORSCANS
|
||||
oss
|
||||
@@ -1140,7 +1130,7 @@ pdo
|
||||
pdto
|
||||
pdtobj
|
||||
pdw
|
||||
Peb
|
||||
peb
|
||||
pef
|
||||
PElems
|
||||
Pels
|
||||
@@ -1270,7 +1260,6 @@ QUERYENDSESSION
|
||||
QUERYOPEN
|
||||
QUEUESYNC
|
||||
QUNS
|
||||
raf
|
||||
RAII
|
||||
RAlt
|
||||
Rasterize
|
||||
@@ -1374,8 +1363,6 @@ runtimes
|
||||
ruuid
|
||||
rvm
|
||||
rwin
|
||||
rwl
|
||||
rwz
|
||||
sacl
|
||||
safeprojectname
|
||||
SAMEKEYPREVIOUSLYMAPPED
|
||||
@@ -1509,7 +1496,6 @@ Srch
|
||||
SRCINVERT
|
||||
SRCPAINT
|
||||
SResize
|
||||
srf
|
||||
srme
|
||||
srre
|
||||
srw
|
||||
@@ -1584,6 +1570,8 @@ SYSLIB
|
||||
SYSMENU
|
||||
SYSTEMAPPS
|
||||
SYSTEMTIME
|
||||
SYSTEMWOW
|
||||
tailwindcss
|
||||
tapp
|
||||
TApplication
|
||||
TApplied
|
||||
@@ -1627,6 +1615,7 @@ tkconverters
|
||||
TLayout
|
||||
tlb
|
||||
tlbimp
|
||||
tlhelp
|
||||
TMPVAR
|
||||
TNP
|
||||
Toolhelp
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
"PowerToys.WorkspacesLauncherUI.exe",
|
||||
"PowerToys.WorkspacesLauncherUI.dll",
|
||||
"PowerToys.WorkspacesModuleInterface.dll",
|
||||
"PowerToys.WorkspacesCsharpLibrary.dll",
|
||||
|
||||
"WinUI3Apps\\PowerToys.RegistryPreviewExt.dll",
|
||||
"WinUI3Apps\\PowerToys.RegistryPreviewUILib.dll",
|
||||
|
||||
@@ -37,6 +37,10 @@ parameters:
|
||||
type: boolean
|
||||
displayName: "Run Tests"
|
||||
default: true
|
||||
- name: useVSPreview
|
||||
type: boolean
|
||||
displayName: "Build Using Visual Studio Preview"
|
||||
default: false
|
||||
|
||||
extends:
|
||||
template: templates/pipeline-ci-build.yml
|
||||
@@ -44,3 +48,4 @@ extends:
|
||||
buildPlatforms: ${{ parameters.buildPlatforms }}
|
||||
enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }}
|
||||
runTests: ${{ parameters.runTests }}
|
||||
useVSPreview: ${{ parameters.useVSPreview }}
|
||||
|
||||
@@ -33,6 +33,11 @@ parameters:
|
||||
- x64
|
||||
- arm64
|
||||
|
||||
- name: useVSPreview
|
||||
type: boolean
|
||||
displayName: "Build Using Visual Studio Preview"
|
||||
default: false
|
||||
|
||||
name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr)
|
||||
|
||||
extends:
|
||||
@@ -42,7 +47,10 @@ extends:
|
||||
- 1ES.PT.ViaStartRight
|
||||
pool:
|
||||
name: SHINE-INT-S
|
||||
image: SHINE-VS17-Latest
|
||||
${{ if eq(parameters.useVSPreview, true) }}:
|
||||
demands: ImageOverride -equals SHINE-VS17-Preview
|
||||
${{ else }}:
|
||||
image: SHINE-VS17-Latest
|
||||
os: windows
|
||||
sdl:
|
||||
tsa:
|
||||
@@ -58,7 +66,10 @@ extends:
|
||||
parameters:
|
||||
pool:
|
||||
name: SHINE-INT-L
|
||||
image: SHINE-VS17-Latest
|
||||
${{ if eq(parameters.useVSPreview, true) }}:
|
||||
demands: ImageOverride -equals SHINE-VS17-Preview
|
||||
${{ else }}:
|
||||
image: SHINE-VS17-Latest
|
||||
os: windows
|
||||
variables:
|
||||
IsPipeline: 1 # The installer uses this to detect whether it should pick up localizations
|
||||
|
||||
@@ -50,7 +50,9 @@ parameters:
|
||||
- name: runTests
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
- name: useVSPreview
|
||||
type: boolean
|
||||
default: false
|
||||
- name: versionNumber
|
||||
type: string
|
||||
default: '0.0.1'
|
||||
@@ -135,7 +137,7 @@ jobs:
|
||||
- template: steps-ensure-dotnet-version.yml
|
||||
parameters:
|
||||
sdk: true
|
||||
version: '8.0'
|
||||
version: '9.0'
|
||||
|
||||
- ${{ if eq(parameters.runTests, true) }}:
|
||||
- task: VisualStudioTestPlatformInstaller@1
|
||||
@@ -181,6 +183,9 @@ jobs:
|
||||
- pwsh: |-
|
||||
& "$(build.sourcesdirectory)\.pipelines\verifyAndSetLatestVCToolsVersion.ps1"
|
||||
displayName: Work around DD-1541167 (VCToolsVersion)
|
||||
${{ if eq(parameters.useVSPreview, true) }}:
|
||||
env:
|
||||
VCWhereExtraVersionTarget: '-prerelease'
|
||||
|
||||
- pwsh: |-
|
||||
& "$(build.sourcesdirectory)\.pipelines\installWiX.ps1"
|
||||
@@ -228,7 +233,10 @@ jobs:
|
||||
inputs:
|
||||
solution: '**\HostsUILib.csproj'
|
||||
vsVersion: 17.0
|
||||
msbuildArgs: /p:CIBuild=true;NoBuild=true -t:pack /bl:$(LogOutputDirectory)\build-hosts.binlog
|
||||
${{ if eq(parameters.useVSPreview, true) }}:
|
||||
msbuildArgs: /p:CIBuild=true;NoBuild=true -t:pack /bl:$(LogOutputDirectory)\build-hosts.binlog /p:NoWarn=NU5104
|
||||
${{ else }}:
|
||||
msbuildArgs: /p:CIBuild=true;NoBuild=true -t:pack /bl:$(LogOutputDirectory)\build-hosts.binlog
|
||||
configuration: $(BuildConfiguration)
|
||||
msbuildArchitecture: x64
|
||||
maximumCpuCount: true
|
||||
@@ -241,7 +249,10 @@ jobs:
|
||||
inputs:
|
||||
solution: '**\EnvironmentVariablesUILib.csproj'
|
||||
vsVersion: 17.0
|
||||
msbuildArgs: /p:CIBuild=true;NoBuild=true -t:pack /bl:$(LogOutputDirectory)\build-env-var-editor.binlog
|
||||
${{ if eq(parameters.useVSPreview, true) }}:
|
||||
msbuildArgs: /p:CIBuild=true;NoBuild=true -t:pack /bl:$(LogOutputDirectory)\build-env-var-editor.binlog /p:NoWarn=NU5104
|
||||
${{ else }}:
|
||||
msbuildArgs: /p:CIBuild=true;NoBuild=true -t:pack /bl:$(LogOutputDirectory)\build-env-var-editor.binlog
|
||||
configuration: $(BuildConfiguration)
|
||||
msbuildArchitecture: x64
|
||||
maximumCpuCount: true
|
||||
@@ -254,7 +265,10 @@ jobs:
|
||||
inputs:
|
||||
solution: '**\RegistryPreviewUILib.csproj'
|
||||
vsVersion: 17.0
|
||||
msbuildArgs: /p:CIBuild=true;NoBuild=true -t:pack /bl:$(LogOutputDirectory)\build-registry-preview.binlog
|
||||
${{ if eq(parameters.useVSPreview, true) }}:
|
||||
msbuildArgs: /p:CIBuild=true;NoBuild=true -t:pack /bl:$(LogOutputDirectory)\build-registry-preview.binlog /p:NoWarn=NU5104
|
||||
${{ else }}:
|
||||
msbuildArgs: /p:CIBuild=true;NoBuild=true -t:pack /bl:$(LogOutputDirectory)\build-registry-preview.binlog
|
||||
configuration: $(BuildConfiguration)
|
||||
msbuildArchitecture: x64
|
||||
maximumCpuCount: true
|
||||
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
- template: steps-ensure-dotnet-version.yml
|
||||
parameters:
|
||||
sdk: true
|
||||
version: '8.0'
|
||||
version: '9.0'
|
||||
|
||||
- task: VisualStudioTestPlatformInstaller@1
|
||||
displayName: Ensure VSTest Platform
|
||||
|
||||
@@ -19,6 +19,9 @@ parameters:
|
||||
- name: runTests
|
||||
type: boolean
|
||||
default: true
|
||||
- name: useVSPreview
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
stages:
|
||||
# Allow manual builds to skip pre-check
|
||||
@@ -43,12 +46,15 @@ stages:
|
||||
name: SHINE-INT-L
|
||||
${{ else }}:
|
||||
name: SHINE-OSS-L
|
||||
${{ if eq(parameters.useVSPreview, true) }}:
|
||||
demands: ImageOverride -equals SHINE-VS17-Preview
|
||||
buildPlatforms:
|
||||
- ${{ platform }}
|
||||
buildConfigurations: [Release]
|
||||
enablePackageCaching: true
|
||||
enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }}
|
||||
runTests: ${{ parameters.runTests }}
|
||||
useVSPreview: ${{ parameters.useVSPreview }}
|
||||
|
||||
- ${{ if eq(parameters.runTests, true) }}:
|
||||
- stage: Test_${{ platform }}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
parameters:
|
||||
- name: version
|
||||
type: string
|
||||
default: "8.0"
|
||||
default: "9.0"
|
||||
- name: sdk
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
$LatestVCToolsVersion = (([xml](& 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' -latest -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -include packages -format xml)).instances.instance.packages.package | ? { $_.id -eq "Microsoft.VisualCpp.CRT.Source" }).version;
|
||||
$LatestVCToolsVersion = (([xml](& 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' -latest $env:VCWhereExtraVersionTarget -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -include packages -format xml)).instances.instance.packages.package | ? { $_.id -eq "Microsoft.VisualCpp.CRT.Source" }).version;
|
||||
|
||||
Write-Output "Latest VCToolsVersion: $LatestVCToolsVersion"
|
||||
Write-Output "Updating VCToolsVersion environment variable for job"
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
# PowerToys Contributor's Guide
|
||||
|
||||
Below is our guidance for how to report issues, propose new features, and submit contributions via Pull Requests (PRs). Our philosophy is heavily based around understanding the problem and scenarios first, this is why we follow this pattern before work has started.
|
||||
Below is our guidance for reporting issues, proposing new features, and submitting contributions via Pull Requests (PRs). Our philosophy is to understand the problem and scenarios first, which is why we follow this pattern before work starts.
|
||||
|
||||
1. There is an issue
|
||||
2. There has been a conversation
|
||||
3. There is agreement on the problem, the fit for PowerToys, and the solution to the problem (implementation)
|
||||
1. There is an issue.
|
||||
2. There has been a conversation.
|
||||
3. There is agreement on the problem, the fit for PowerToys, and the solution to the problem (implementation).
|
||||
|
||||
## Filing an issue
|
||||
## Filing an Issue
|
||||
|
||||
Please follow this simple rule to help us eliminate any unnecessary wasted effort & frustration, and ensure an efficient and effective use of everyone's time - yours, ours, and other community members':
|
||||
**Importance of Filing an Issue First**
|
||||
|
||||
> 👉 If you have a question, think you've discovered an issue, would like to propose a new feature, etc., then find/file an issue **BEFORE** starting work to fix/implement it.
|
||||
Please follow this rule to help eliminate wasted effort and frustration, and to ensure an efficient and effective use of everyone’s time:
|
||||
|
||||
When requesting new features / enhancements, understanding problem and scenario around it is extremely important. Having additional evidence, data, tweets, blog posts, research, ... anything is extremely helpful. This information provides context to the scenario that may otherwise be lost.
|
||||
> 👉 If you have a question, think you've discovered an issue, or would like to propose a new feature, please find/file an issue **BEFORE** starting work to fix/implement it.
|
||||
|
||||
* Don't know whether you're reporting an issue or requesting a feature? File an issue
|
||||
* Have a question that you don't see answered in docs, videos, etc.? File an issue
|
||||
* Want to know if we're planning on building a particular feature? File an issue
|
||||
* Got a great idea for a new utility or feature? File an issue/request/idea
|
||||
* Don't understand how to do something? File an issue/Community Guidance Request
|
||||
* Found an existing issue that describes yours? Great - upvote and add additional commentary / info / repro-steps / etc.
|
||||
When requesting new features or enhancements, providing additional evidence, data, tweets, blog posts, or research is extremely helpful. This information gives context to the scenario that may otherwise be lost.
|
||||
|
||||
A quick search before filing an issue also could be helpful. It is likely someone else has found the problem you're seeing, and someone may be working on or have already contributed a fix!
|
||||
* Unsure whether it’s an issue or feature request? File an issue.
|
||||
* Have a question that isn't answered in the docs, videos, etc.? File an issue.
|
||||
* Want to know if we’re planning a particular feature? File an issue.
|
||||
* Got a great idea for a new utility or feature? File an issue/request/idea.
|
||||
* Don’t understand how to do something? File an issue/Community Guidance Request.
|
||||
* Found an existing issue that describes yours? Great! Upvote and add additional commentary, info, or repro steps.
|
||||
|
||||
### How to tell the PowerToys team this is an interesting thing to focus on
|
||||
A quick search before filing an issue could be helpful. It’s likely someone else has found the same problem, and they may even be working on or have already contributed a fix!
|
||||
|
||||
Upvote the original issue by clicking its [+😊] button and hitting 👍 (+1) icon or a different one. This way allows us to measure how impactful different issues are compared to others. The issue with comments like "+1", "me too", or similar is they actually make it harder to have a conversation and harder to quickly determine trending important requests.
|
||||
### Indicating Interest in Issues
|
||||
|
||||
To let the team know which issues are important, upvote by clicking the [+😊] button and the 👍 icon on the original issue post. Avoid comments like "+1" or "me too" as they clutter the discussion and make it harder to prioritize requests.
|
||||
|
||||
---
|
||||
|
||||
## Contributing fixes / features
|
||||
## Contributing Fixes/Features
|
||||
|
||||
Please comment on [our "Would you like to contribute to PowerToys?" thread](https://github.com/microsoft/PowerToys/issues/28769) to let us know you're interested in working on something before you start the work. Not only does this avoid multiple people unexpectedly working on the same thing at the same time but it enables us to make sure everyone is clear on what should be done to implement any new functionality. It's less work for everyone, in the long run, to establish this up front.
|
||||
Please comment on our ["Would you like to contribute to PowerToys?"](https://github.com/microsoft/PowerToys/issues/28769) thread to let us know you're interested in working on something before you start. This helps avoid multiple people unexpectedly working on the same thing and ensures everyone is clear on what should be done. It's less work for everyone to establish this up front.
|
||||
|
||||
### Localization issues
|
||||
### Localization Issues
|
||||
|
||||
Please file localization issues, so our internal localization team can identify and fix them. However we currently don't accept community Pull Requests fixing localization issues. Localization is handled by the internal Microsoft team only.
|
||||
For localization issues, please file an issue to notify our internal localization team, as community PRs for localization aren't accepted. Localization is handled exclusively by the internal Microsoft team.
|
||||
|
||||
### To Spec or not to Spec
|
||||
### To Spec or Not to Spec
|
||||
|
||||
A key point is for everyone to understand the approach that will be taken. We want to be sure if anyone does work, we will accept it in. Items that are larger in scope we'll want some type of spec to understand what is being planned and have a discussion. Specs help collaborators discuss different approaches to solve a problem, describe how the feature will behave, how the feature will impact the user, what happens if something goes wrong, etc. Driving towards agreement in a spec, before any code is written, often results in simpler code, and less wasted effort in the long run.
|
||||
A key point is for everyone to understand the approach that will be taken. We want to be sure that any work done will be accepted. Larger-scope items will require a spec to outline the approach and allow for discussion. Specs help collaborators consider different solutions, describe feature behavior, and plan for errors. Achieving agreement in a spec before writing code often results in simpler code and less wasted effort.
|
||||
|
||||
For such scenarios, once a team member has agreed with your approach, skip ahead to the section headed "Development" section below.
|
||||
|
||||
Team members will be happy to help review specs and guide them to completion.
|
||||
Once a team member has agreed with your approach, proceed to the "Development" section below. Team members are happy to help review specs and guide them to completion.
|
||||
|
||||
### Help Wanted
|
||||
|
||||
Once the team has approved an issue/spec approach to solving, development can proceed. If no developers are immediately available, the spec can be parked ready for a developer to get started. Parked specs' issues will be labeled "Help Wanted". To find a list of development opportunities waiting for developer involvement, visit the Issues and filter on [the Help-Wanted label](https://github.com/microsoft/PowerToys/labels/Help%20Wanted).
|
||||
Once the team has approved an issue/spec approach, development can proceed. If no developers are immediately available, the spec may be parked and labeled "Help Wanted," ready for a developer to get started. For development opportunities, visit [Issues labeled Help Wanted](https://github.com/microsoft/PowerToys/labels/Help%20Wanted).
|
||||
|
||||
---
|
||||
|
||||
@@ -55,18 +55,18 @@ Once the team has approved an issue/spec approach to solving, development can pr
|
||||
|
||||
Follow the [development guidelines](https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/readme.md).
|
||||
|
||||
### Naming of features and functionality
|
||||
### Naming Features and Functionality
|
||||
|
||||
Naming should be descriptive and straight forward. We want names to be clear about functionality and usefulness moving forward.
|
||||
Names should be descriptive and straightforward, clearly reflecting functionality and usefulness.
|
||||
|
||||
### How can I become a collaborator on the PowerToys team
|
||||
### Becoming a Collaborator on the PowerToys Team
|
||||
|
||||
Be a great community member. Just help out a lot and make useful additions, filing bugs/suggestions, help develop fixes and features, code reviews, and always, docs. Lets continue to make the PowerToys repository a great spot to learn and make a great set of utilities.
|
||||
Be an active community member! Make helpful contributions by filing bugs, offering suggestions, developing fixes and features, conducting code reviews, and updating documentation.
|
||||
|
||||
When the time comes, Microsoft will reach out and help make you a formal team member. Just make sure they can reach out to you :)
|
||||
When the time comes, Microsoft will reach out to you about becoming a formal team member. Just make sure they have a way to contact you. 😊
|
||||
|
||||
---
|
||||
|
||||
## Thank you
|
||||
## Thank You
|
||||
|
||||
Thank you in advance for your contribution!
|
||||
Thank you in advance for your contribution! We appreciate your help in making PowerToys a better tool for everyone.
|
||||
|
||||
@@ -67,6 +67,10 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.TrayFlyoutModuleRunEvent</td>
|
||||
<td>Logs when a utility from the tray flyout menu is run.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.Uninstall_Success</td>
|
||||
<td>Logs when PowerToys is successfully uninstalled (who would do such a thing!).</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### OOBE (Out-of-box experience)
|
||||
@@ -139,6 +143,10 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.AdvancedPasteInAppKeyboardShortcutEvent</td>
|
||||
<td>Triggered when a keyboard shortcut is used within the Advanced Paste interface.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.AdvancedPasteSemanticKernelFormatEvent</td>
|
||||
<td>Triggered when Advanced Paste leverages the Semantic Kernel.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### Always on Top
|
||||
|
||||
@@ -30,19 +30,19 @@
|
||||
<PackageVersion Include="Markdig.Signed" Version="0.34.0" />
|
||||
<!-- Including MessagePack to force version, since it's used by StreamJsonRpc but contains vulnerabilities. After StreamJsonRpc updates the version of MessagePack, we can upgrade StreamJsonRpc instead. -->
|
||||
<PackageVersion Include="MessagePack" Version="2.5.187" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.7" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0-preview.24508.2" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.16" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
|
||||
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.2739.15" />
|
||||
<!-- Package Microsoft.Win32.SystemEvents 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.Drawing.Common but the 8.0.1 version wasn't published to nuget. -->
|
||||
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Windows.Compatibility" Version="8.0.10" />
|
||||
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Windows.Compatibility" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.2.46-beta" />
|
||||
<!-- CsWinRT version needs to be set to have a WinRT.Runtime.dll at the same version contained inside the NET SDK we're currently building on CI. -->
|
||||
<!--
|
||||
@@ -67,24 +67,26 @@
|
||||
<PackageVersion Include="StreamJsonRpc" Version="2.19.27" />
|
||||
<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="8.0.0" />
|
||||
<PackageVersion Include="System.CodeDom" Version="9.0.0" />
|
||||
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
<PackageVersion Include="System.ComponentModel.Composition" Version="8.0.0" />
|
||||
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="8.0.1" />
|
||||
<PackageVersion Include="System.Data.OleDb" Version="8.0.1" />
|
||||
<PackageVersion Include="System.ComponentModel.Composition" Version="9.0.0" />
|
||||
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="9.0.0" />
|
||||
<PackageVersion Include="System.Data.OleDb" Version="9.0.0" />
|
||||
<!-- Package System.Data.SqlClient added to force it as a dependency of Microsoft.Windows.Compatibility to the latest version available at this time. -->
|
||||
<PackageVersion Include="System.Data.SqlClient" Version="4.8.6" />
|
||||
<!-- Package System.Diagnostics.EventLog 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.Data.OleDb but the 8.0.1 version wasn't published to nuget. -->
|
||||
<PackageVersion Include="System.Diagnostics.EventLog" Version="8.0.1" />
|
||||
<PackageVersion Include="System.Drawing.Common" Version="8.0.7" />
|
||||
<PackageVersion Include="System.Diagnostics.EventLog" Version="9.0.0" />
|
||||
<!-- Package System.Diagnostics.PerformanceCounter added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.11. -->
|
||||
<PackageVersion Include="System.Diagnostics.PerformanceCounter" Version="9.0.0" />
|
||||
<PackageVersion Include="System.Drawing.Common" Version="9.0.0" />
|
||||
<PackageVersion Include="System.IO.Abstractions" Version="21.0.29" />
|
||||
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="21.0.29" />
|
||||
<PackageVersion Include="System.Management" Version="8.0.0" />
|
||||
<PackageVersion Include="System.Management" Version="9.0.0" />
|
||||
<PackageVersion Include="System.Reactive" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Runtime.Caching" Version="8.0.1" />
|
||||
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="8.0.1" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
|
||||
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
|
||||
<PackageVersion Include="System.Runtime.Caching" Version="9.0.0" />
|
||||
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="9.0.0" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.0" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.0" />
|
||||
<PackageVersion Include="UnicodeInformation" Version="2.6.0" />
|
||||
<PackageVersion Include="UnitsNet" Version="5.56.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
||||
|
||||
41
NOTICE.md
41
NOTICE.md
@@ -1318,18 +1318,18 @@ EXHIBIT A -Mozilla Public License.
|
||||
- Mages 2.0.2
|
||||
- Markdig.Signed 0.34.0
|
||||
- MessagePack 2.5.187
|
||||
- Microsoft.CodeAnalysis.NetAnalyzers 8.0.0
|
||||
- Microsoft.Data.Sqlite 8.0.7
|
||||
- Microsoft.CodeAnalysis.NetAnalyzers 9.0.0-preview.24508.2
|
||||
- Microsoft.Data.Sqlite 9.0.0
|
||||
- Microsoft.Diagnostics.Tracing.TraceEvent 3.1.16
|
||||
- Microsoft.Extensions.DependencyInjection 8.0.0
|
||||
- Microsoft.Extensions.Hosting 8.0.0
|
||||
- Microsoft.Extensions.Hosting.WindowsServices 8.0.0
|
||||
- Microsoft.Extensions.Logging 8.0.0
|
||||
- Microsoft.Extensions.Logging.Abstractions 8.0.0
|
||||
- Microsoft.Extensions.DependencyInjection 9.0.0
|
||||
- Microsoft.Extensions.Hosting 9.0.0
|
||||
- Microsoft.Extensions.Hosting.WindowsServices 9.0.0
|
||||
- Microsoft.Extensions.Logging 9.0.0
|
||||
- Microsoft.Extensions.Logging.Abstractions 9.0.0
|
||||
- Microsoft.Toolkit.Uwp.Notifications 7.1.2
|
||||
- Microsoft.Web.WebView2 1.0.2739.15
|
||||
- Microsoft.Win32.SystemEvents 8.0.0
|
||||
- Microsoft.Windows.Compatibility 8.0.10
|
||||
- Microsoft.Win32.SystemEvents 9.0.0
|
||||
- Microsoft.Windows.Compatibility 9.0.0
|
||||
- Microsoft.Windows.CsWin32 0.2.46-beta
|
||||
- Microsoft.Windows.CsWinRT 2.1.5
|
||||
- Microsoft.Windows.SDK.BuildTools 10.0.22621.2428
|
||||
@@ -1346,22 +1346,23 @@ EXHIBIT A -Mozilla Public License.
|
||||
- SharpCompress 0.37.2
|
||||
- StreamJsonRpc 2.19.27
|
||||
- StyleCop.Analyzers 1.2.0-beta.556
|
||||
- System.CodeDom 8.0.0
|
||||
- System.CodeDom 9.0.0
|
||||
- System.CommandLine 2.0.0-beta4.22272.1
|
||||
- System.ComponentModel.Composition 8.0.0
|
||||
- System.Configuration.ConfigurationManager 8.0.1
|
||||
- System.Data.OleDb 8.0.1
|
||||
- System.ComponentModel.Composition 9.0.0
|
||||
- System.Configuration.ConfigurationManager 9.0.0
|
||||
- System.Data.OleDb 9.0.0
|
||||
- System.Data.SqlClient 4.8.6
|
||||
- System.Diagnostics.EventLog 8.0.1
|
||||
- System.Drawing.Common 8.0.7
|
||||
- System.Diagnostics.EventLog 9.0.0
|
||||
- System.Diagnostics.PerformanceCounter 9.0.0
|
||||
- System.Drawing.Common 9.0.0
|
||||
- System.IO.Abstractions 21.0.29
|
||||
- System.IO.Abstractions.TestingHelpers 21.0.29
|
||||
- System.Management 8.0.0
|
||||
- System.Management 9.0.0
|
||||
- System.Reactive 6.0.1
|
||||
- System.Runtime.Caching 8.0.1
|
||||
- System.ServiceProcess.ServiceController 8.0.1
|
||||
- System.Text.Encoding.CodePages 8.0.0
|
||||
- System.Text.Json 8.0.5
|
||||
- System.Runtime.Caching 9.0.0
|
||||
- System.ServiceProcess.ServiceController 9.0.0
|
||||
- System.Text.Encoding.CodePages 9.0.0
|
||||
- System.Text.Json 9.0.0
|
||||
- UnicodeInformation 2.6.0
|
||||
- UnitsNet 5.56.0
|
||||
- UTF.Unknown 2.5.1
|
||||
|
||||
@@ -698,6 +698,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MenusExtension", "src\modul
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirtualDesktopExtension", "src\modules\cmdpal\Exts\VirtualDesktopExtension\VirtualDesktopExtension.csproj", "{B79B52FB-8B2E-4CF5-B0FE-37E3E981AC7A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkspacesCsharpLibrary", "src\modules\Workspaces\WorkspacesCsharpLibrary\WorkspacesCsharpLibrary.csproj", "{89D0E199-B17A-418C-B2F8-7375B6708357}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Project Templates", "Project Templates", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.WindowWalker", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.WindowWalker\Microsoft.CmdPal.Ext.WindowWalker.csproj", "{3A9A7297-92C4-4F16-B6F9-8D4AB652C86C}"
|
||||
EndProject
|
||||
Global
|
||||
@@ -3274,6 +3278,18 @@ Global
|
||||
{B79B52FB-8B2E-4CF5-B0FE-37E3E981AC7A}.Release|x86.ActiveCfg = Release|x64
|
||||
{B79B52FB-8B2E-4CF5-B0FE-37E3E981AC7A}.Release|x86.Build.0 = Release|x64
|
||||
{B79B52FB-8B2E-4CF5-B0FE-37E3E981AC7A}.Release|x86.Deploy.0 = Release|x64
|
||||
{89D0E199-B17A-418C-B2F8-7375B6708357}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{89D0E199-B17A-418C-B2F8-7375B6708357}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{89D0E199-B17A-418C-B2F8-7375B6708357}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{89D0E199-B17A-418C-B2F8-7375B6708357}.Debug|x64.Build.0 = Debug|x64
|
||||
{89D0E199-B17A-418C-B2F8-7375B6708357}.Debug|x86.ActiveCfg = Debug|x64
|
||||
{89D0E199-B17A-418C-B2F8-7375B6708357}.Debug|x86.Build.0 = Debug|x64
|
||||
{89D0E199-B17A-418C-B2F8-7375B6708357}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{89D0E199-B17A-418C-B2F8-7375B6708357}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{89D0E199-B17A-418C-B2F8-7375B6708357}.Release|x64.ActiveCfg = Release|x64
|
||||
{89D0E199-B17A-418C-B2F8-7375B6708357}.Release|x64.Build.0 = Release|x64
|
||||
{89D0E199-B17A-418C-B2F8-7375B6708357}.Release|x86.ActiveCfg = Release|x64
|
||||
{89D0E199-B17A-418C-B2F8-7375B6708357}.Release|x86.Build.0 = Release|x64
|
||||
{3A9A7297-92C4-4F16-B6F9-8D4AB652C86C}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{3A9A7297-92C4-4F16-B6F9-8D4AB652C86C}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{3A9A7297-92C4-4F16-B6F9-8D4AB652C86C}.Debug|x64.ActiveCfg = Debug|x64
|
||||
@@ -3543,7 +3559,7 @@ Global
|
||||
{C831231F-891C-4572-9694-45062534B42A} = {071E18A4-A530-46B8-AB7D-B862EE55E24E}
|
||||
{2C2AF4F4-3DB2-416F-9F38-2B710D961E6F} = {071E18A4-A530-46B8-AB7D-B862EE55E24E}
|
||||
{BE349BD2-7FF8-47B6-8F74-42B52DDF730A} = {071E18A4-A530-46B8-AB7D-B862EE55E24E}
|
||||
{EEBDB1CA-BBDD-421D-9D86-67145D0C6EEB} = {071E18A4-A530-46B8-AB7D-B862EE55E24E}
|
||||
{EEBDB1CA-BBDD-421D-9D86-67145D0C6EEB} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
||||
{8E47BF33-A402-4582-930C-95E35418F653} = {071E18A4-A530-46B8-AB7D-B862EE55E24E}
|
||||
{7520A2FE-00A2-49B8-83ED-DB216E874C04} = {3846508C-77EB-4034-A702-F8BB263C4F79}
|
||||
{8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90} = {7520A2FE-00A2-49B8-83ED-DB216E874C04}
|
||||
@@ -3551,6 +3567,8 @@ Global
|
||||
{D8DD2E06-7956-4673-95E7-F395AB5A5485} = {071E18A4-A530-46B8-AB7D-B862EE55E24E}
|
||||
{8ABE2195-7514-425E-9A89-685FA42CEFC3} = {071E18A4-A530-46B8-AB7D-B862EE55E24E}
|
||||
{B79B52FB-8B2E-4CF5-B0FE-37E3E981AC7A} = {071E18A4-A530-46B8-AB7D-B862EE55E24E}
|
||||
{89D0E199-B17A-418C-B2F8-7375B6708357} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF}
|
||||
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {3846508C-77EB-4034-A702-F8BB263C4F79}
|
||||
{3A9A7297-92C4-4F16-B6F9-8D4AB652C86C} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
|
||||
@@ -101,11 +101,11 @@ In this release, we focused on new features, stability, and improvements.
|
||||
- Advanced Paste has new abilities: Image to text, and paste to file (text / png / html).
|
||||
- In settings, we've adjusted the left navigation to group the utilities. As the number of utilities shipped with PowerToys keeps growing, we felt this was a needed adjustment. Thanks everyone for your feedback!
|
||||
- Workspaces received many bug fixes, including the proper launching of many instances of the same application in the same workspace. Note, we are still actively looking at how to properly handle PWA detection.
|
||||
- We've added a telemetry opt-in option in the Settings General tab. As it is off-by-default, we encourage users to turn it on as that helps direct our development efforts and their journeys. More information about the data we collect can be found in the [PowerToys Data and Privacy documentation](https://aka.ms/powertoys-data-and-privacy-documentation) and what each event does.
|
||||
- We've added a diagnostic data (telemetry) opt-in option in the Settings General tab. As it is off-by-default, we encourage users to turn it on as that helps direct our development efforts and their journeys. More information about the data we collect can be found in the [PowerToys Data and Privacy documentation](https://aka.ms/powertoys-data-and-privacy-documentation) and what each event does.
|
||||
|
||||
### General
|
||||
|
||||
- Added a setting for telemetry opt-in (off by default, however, see above for why we encourage you to opt-in!) and user controls to view data.
|
||||
- Added a setting for diagnostic data (telemetry) opt-in (off by default, however, see above for why we encourage you to opt-in!) and user controls to view data.
|
||||
- Improved exception logging by adding the type of Exception and InnerException. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
|
||||
### Advanced Paste
|
||||
@@ -204,7 +204,7 @@ This project has adopted the [Microsoft Open Source Code of Conduct][oss-conduct
|
||||
|
||||
## Privacy Statement
|
||||
|
||||
The application logs basic telemetry. Our Telemetry Data page (Coming Soon) has the trends from the telemetry. Please read the [Microsoft privacy statement][privacy-link] for more information.
|
||||
The application logs basic diagnostic data (telemetry). For more information on privacy 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
|
||||
|
||||
@@ -39,6 +39,8 @@ Contact the developers of a plugin directly for assistance with a specific plugi
|
||||
| [GitHubRepo](https://github.com/8LWXpg/PowerToysRun-GitHubRepo) | [8LWXpg](https://github.com/8LWXpg) | Search and open GitHub repositories |
|
||||
| [ProcessKiller](https://github.com/8LWXpg/PowerToysRun-ProcessKiller) | [8LWXpg](https://github.com/8LWXpg) | Search and kill processes |
|
||||
| [ChatGPT](https://github.com/ferraridavide/ChatGPTPowerToys) | [ferraridavide](https://github.com/ferraridavide) | Ask a question to ChatGPT |
|
||||
| [CanIUse](https://github.com/skttl/ptrun-caniuse) | [skttl](https://github.com/skttl) | Look up browser feature support with caniuse.com |
|
||||
| [TailwindCSS](https://github.com/skttl/ptrun-tailwindcss) | [skttl](https://github.com/skttl) | Search the documentation of TailwindCSS |
|
||||
|
||||
## Extending software plugins
|
||||
|
||||
@@ -58,3 +60,4 @@ Below are community created plugins that target a website or software. They are
|
||||
| [PowerSearch for 1Password](https://github.com/KairuDeibisu/PowerToysRunPlugin1Password) | [KairuDeibisu](https://github.com/KairuDeibisu) | An unofficial plugin for searching 1Password for usernames and passwords |
|
||||
| [HackMD](https://github.com/8LWXpg/PowerToysRun-HackMD) | [8LWXpg](https://github.com/8LWXpg) | Open HackMD notes |
|
||||
| [SSH](https://github.com/8LWXpg/PowerToysRun-SSH) | [8LWXpg](https://github.com/8LWXpg) | Connect to ssh clients |
|
||||
| [Bilibili](https://github.com/Whuihuan/PowerToysRun-Bilibili) | [Whuihuan](https://github.com/Whuihuan) | Use AVID or BVID to parse and jump to Bilibili |
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<WindowsSdkPackageVersion>10.0.22621.48</WindowsSdkPackageVersion>
|
||||
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
|
||||
<TargetFramework>net9.0-windows10.0.22621.0</TargetFramework>
|
||||
<TargetPlatformMinVersion>10.0.19041.0</TargetPlatformMinVersion>
|
||||
<SupportedOSPlatformVersion>10.0.19041.0</SupportedOSPlatformVersion>
|
||||
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
|
||||
@@ -14,7 +14,7 @@
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<NoWarn></NoWarn>
|
||||
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
|
||||
<WarningsNotAsErrors>CA1720</WarningsNotAsErrors>
|
||||
<WarningsNotAsErrors>CA1720;CA1859;CA2263;CA2022;MVVMTK0045;MVVMTK0049</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
|
||||
|
||||
@@ -7,7 +7,7 @@ export async function registerAdditionalLanguages(monaco){
|
||||
await languageDefinitions();
|
||||
registerAdditionalLanguage("cppExt", [".ino", ".pde"], "cpp", monaco)
|
||||
registerAdditionalLanguage("xmlExt", [".wsdl", ".csproj", ".vcxproj", ".vbproj", ".fsproj"], "xml", monaco)
|
||||
registerAdditionalLanguage("txtExt", [".sln", ".log", ".vsconfig", ".env", ".srt"], "txt", monaco)
|
||||
registerAdditionalLanguage("txtExt", [".sln", ".log", ".vsconfig", ".env", ".srt", ".ahk"], "txt", monaco)
|
||||
registerAdditionalLanguage("razorExt", [".razor"], "razor", monaco)
|
||||
registerAdditionalLanguage("vbExt", [".vbs"], "vb", monaco)
|
||||
registerAdditionalLanguage("iniExt", [".inf", ".gitconfig", ".gitattributes", ".editorconfig"], "ini", monaco)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,7 +2,6 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO.Abstractions;
|
||||
@@ -25,7 +24,7 @@ namespace AllExperiments
|
||||
}
|
||||
|
||||
// Using InvariantCulture since this is used for a log file name
|
||||
var logFilePath = Path.Combine(ApplicationLogPath, "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".txt");
|
||||
string logFilePath = Path.Combine(ApplicationLogPath, "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".txt");
|
||||
|
||||
Trace.Listeners.Add(new TextWriterTraceListener(logFilePath));
|
||||
|
||||
@@ -71,10 +70,10 @@ namespace AllExperiments
|
||||
|
||||
private static string GetCallerInfo()
|
||||
{
|
||||
StackTrace stackTrace = new StackTrace();
|
||||
StackTrace stackTrace = new();
|
||||
|
||||
var methodName = stackTrace.GetFrame(3)?.GetMethod();
|
||||
var className = methodName?.DeclaringType?.Name;
|
||||
System.Reflection.MethodBase? methodName = stackTrace.GetFrame(3)?.GetMethod();
|
||||
string? className = methodName?.DeclaringType?.Name;
|
||||
return "[Method]: " + methodName?.Name + " [Class]: " + className;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<AssemblyName>PowerToys.Common.UI</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -29,34 +29,28 @@ namespace Microsoft.PowerToys.FilePreviewCommon
|
||||
new XmlFormatter(),
|
||||
}.AsReadOnly();
|
||||
|
||||
private static string? _monacoDirectory;
|
||||
private static readonly Lazy<string> _monacoDirectory = new(GetRuntimeMonacoDirectory);
|
||||
|
||||
public static string GetRuntimeMonacoDirectory()
|
||||
/// <summary>
|
||||
/// Gets the path of the Monaco assets folder.
|
||||
/// </summary>
|
||||
public static string MonacoDirectory => _monacoDirectory.Value;
|
||||
|
||||
private static string GetRuntimeMonacoDirectory()
|
||||
{
|
||||
string codeBase = Assembly.GetExecutingAssembly().Location;
|
||||
string path = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(codeBase) ?? string.Empty, "Assets", "Monaco"));
|
||||
if (Path.Exists(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We're likely in WinUI3Apps directory and need to go back to the base directory.
|
||||
return Path.GetFullPath(Path.Combine(Path.GetDirectoryName(codeBase) ?? string.Empty, "..", "Assets", "Monaco"));
|
||||
}
|
||||
}
|
||||
string exePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? string.Empty;
|
||||
|
||||
public static string MonacoDirectory
|
||||
{
|
||||
get
|
||||
// If the executable is within "WinUI3Apps", correct the path first.
|
||||
if (Path.GetFileName(exePath) == "WinUI3Apps")
|
||||
{
|
||||
if (string.IsNullOrEmpty(_monacoDirectory))
|
||||
{
|
||||
_monacoDirectory = GetRuntimeMonacoDirectory();
|
||||
}
|
||||
|
||||
return _monacoDirectory;
|
||||
exePath = Path.Combine(exePath, "..");
|
||||
}
|
||||
|
||||
string monacoPath = Path.Combine(exePath, "Assets", "Monaco");
|
||||
|
||||
return Directory.Exists(monacoPath) ?
|
||||
monacoPath :
|
||||
throw new DirectoryNotFoundException($"Monaco assets directory not found at {monacoPath}");
|
||||
}
|
||||
|
||||
public static JsonDocument GetLanguages()
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace AdvancedPaste.Settings
|
||||
private readonly SettingsUtils _settingsUtils;
|
||||
private readonly TaskScheduler _taskScheduler;
|
||||
private readonly IFileSystemWatcher _watcher;
|
||||
private readonly object _loadingSettingsLock = new();
|
||||
private readonly Lock _loadingSettingsLock = new();
|
||||
private readonly List<PasteFormats> _additionalActions;
|
||||
private readonly List<AdvancedPasteCustomAction> _customActions;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
|
||||
<TargetFramework>net9.0-windows10.0.22621.0</TargetFramework>
|
||||
<TargetPlatformMinVersion>10.0.19041.0</TargetPlatformMinVersion>
|
||||
<SupportedOSPlatformVersion>10.0.19041.0</SupportedOSPlatformVersion>
|
||||
<PublishDir>$(PowerToysRoot)\$(Platform)\$(Configuration)\WinUI3Apps</PublishDir>
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace Hosts.Settings
|
||||
|
||||
private readonly SettingsUtils _settingsUtils;
|
||||
private readonly IFileSystemWatcher _watcher;
|
||||
private readonly object _loadingSettingsLock = new object();
|
||||
private readonly Lock _loadingSettingsLock = new Lock();
|
||||
|
||||
public bool ShowStartupWarning { get; private set; }
|
||||
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace MouseJumpUI.Helpers;
|
||||
|
||||
internal sealed class ThrottledActionInvoker
|
||||
{
|
||||
private readonly object _invokerLock = new();
|
||||
private readonly Lock _invokerLock = new();
|
||||
private readonly DispatcherTimer _timer;
|
||||
|
||||
private Action? _actionToRun;
|
||||
|
||||
@@ -368,7 +368,7 @@ namespace MouseWithoutBorders
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly object ClipboardThreadOldLock = new();
|
||||
private static readonly Lock ClipboardThreadOldLock = new();
|
||||
private static System.Threading.Thread clipboardThreadOld;
|
||||
|
||||
internal static void GetRemoteClipboard(string postAction)
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace MouseWithoutBorders
|
||||
{
|
||||
public class Thread
|
||||
{
|
||||
private static readonly object ThreadsLock = new();
|
||||
private static readonly Lock ThreadsLock = new();
|
||||
private static List<System.Threading.Thread> threads;
|
||||
|
||||
private readonly System.Threading.Thread thread;
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace MouseWithoutBorders
|
||||
|
||||
internal partial class Common
|
||||
{
|
||||
private static readonly object McMatrixLock = new();
|
||||
private static readonly Lock McMatrixLock = new();
|
||||
|
||||
internal const byte MAX_MACHINE = 4;
|
||||
internal const byte MAX_SOCKET = MAX_MACHINE * 2;
|
||||
|
||||
@@ -474,7 +474,7 @@ namespace MouseWithoutBorders
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly object InputSimulationLock = new();
|
||||
private static readonly Lock InputSimulationLock = new();
|
||||
|
||||
internal static void DoSomethingInTheInputSimulationThread(ThreadStart target)
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace MouseWithoutBorders.Class
|
||||
{
|
||||
@@ -36,12 +37,12 @@ namespace MouseWithoutBorders.Class
|
||||
/// </remarks>
|
||||
internal class MachinePool
|
||||
{
|
||||
private readonly object @lock;
|
||||
private readonly Lock @lock;
|
||||
private readonly List<MachineInf> list;
|
||||
|
||||
public MachinePool()
|
||||
{
|
||||
@lock = new object();
|
||||
@lock = new Lock();
|
||||
list = new List<MachineInf>();
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ using System.IO.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
@@ -44,7 +45,7 @@ namespace MouseWithoutBorders.Class
|
||||
internal bool Changed;
|
||||
|
||||
private readonly SettingsUtils _settingsUtils;
|
||||
private readonly object _loadingSettingsLock = new object();
|
||||
private readonly Lock _loadingSettingsLock = new Lock();
|
||||
private readonly IFileSystemWatcher _watcher;
|
||||
|
||||
private MouseWithoutBordersProperties _properties;
|
||||
|
||||
@@ -826,7 +826,7 @@ namespace MouseWithoutBorders.Class
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, List<IPAddress>> BadIPs = new();
|
||||
private static readonly object BadIPsLock = new();
|
||||
private static readonly Lock BadIPsLock = new();
|
||||
|
||||
private static bool IsBadIP(string machineName, IPAddress ip)
|
||||
{
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace MouseWithoutBorders
|
||||
|
||||
[Category("Appearance")]
|
||||
[Description("The thickness of the border around the field")]
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
|
||||
public int BorderSize
|
||||
{
|
||||
get => _borderSize;
|
||||
@@ -33,6 +34,7 @@ namespace MouseWithoutBorders
|
||||
|
||||
[Category("Appearance")]
|
||||
[Description("The color of the border around the field")]
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
|
||||
public Color BorderColor
|
||||
{
|
||||
get => _borderColor;
|
||||
@@ -47,6 +49,7 @@ namespace MouseWithoutBorders
|
||||
|
||||
[Category("Appearance")]
|
||||
[Description("The color of the border around the field when it has focus")]
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
|
||||
public Color FocusColor
|
||||
{
|
||||
get => _focusColor;
|
||||
@@ -59,12 +62,14 @@ namespace MouseWithoutBorders
|
||||
|
||||
[Category("Behavior")]
|
||||
[Description("The maximum number of characters that can be typed in the field")]
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
|
||||
public int MaximumLength
|
||||
{
|
||||
get => InnerField.MaxLength;
|
||||
set => InnerField.MaxLength = value;
|
||||
}
|
||||
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
public override string Text
|
||||
{
|
||||
get => InnerField.Text;
|
||||
|
||||
@@ -19,12 +19,14 @@ namespace MouseWithoutBorders
|
||||
|
||||
[Category("Appearance")]
|
||||
[Description("Image to show when Mouse is pressed on button")]
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
|
||||
public Image DownImage { get; set; }
|
||||
|
||||
private Image _normalImage;
|
||||
|
||||
[Category("Appearance")]
|
||||
[Description("Image to show when button is in normal state")]
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
|
||||
public Image NormalImage
|
||||
{
|
||||
get => _normalImage;
|
||||
@@ -37,10 +39,12 @@ namespace MouseWithoutBorders
|
||||
|
||||
[Category("Appearance")]
|
||||
[Description("Image to show when Mouse hovers over button")]
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
|
||||
public Image HoverImage { get; set; }
|
||||
|
||||
[Category("Appearance")]
|
||||
[Description("Image to show when button is disabled")]
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
|
||||
public Image DisabledImage { get; set; }
|
||||
|
||||
private bool _hovering;
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace MouseWithoutBorders
|
||||
|
||||
[Category("Appearance")]
|
||||
[Description("The bounding rectangle of the check image in local co-ordinates")]
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
|
||||
public Point ImageLocation
|
||||
{
|
||||
get => _imageLocation;
|
||||
@@ -28,6 +29,7 @@ namespace MouseWithoutBorders
|
||||
|
||||
private Point _textLocation;
|
||||
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
|
||||
public Point TextLocation
|
||||
{
|
||||
get => _textLocation;
|
||||
@@ -47,6 +49,7 @@ namespace MouseWithoutBorders
|
||||
|
||||
[Category("Appearance")]
|
||||
[Description("Image to show when Mouse is pressed on button")]
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
|
||||
public Image CheckedImage
|
||||
{
|
||||
get => _checkedImage;
|
||||
@@ -61,6 +64,7 @@ namespace MouseWithoutBorders
|
||||
|
||||
[Category("Appearance")]
|
||||
[Description("Image to show when button is in normal state")]
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
|
||||
public Image UncheckedImage
|
||||
{
|
||||
get => _uncheckedImage;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Windows.Forms;
|
||||
|
||||
// <summary>
|
||||
@@ -34,12 +35,14 @@ namespace MouseWithoutBorders
|
||||
MachineEnabled = false;
|
||||
}
|
||||
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
internal string MachineName
|
||||
{
|
||||
get => textBoxName.Text;
|
||||
set => textBoxName.Text = value;
|
||||
}
|
||||
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
internal bool MachineEnabled
|
||||
{
|
||||
get => checkBoxEnabled.Checked;
|
||||
@@ -52,6 +55,7 @@ namespace MouseWithoutBorders
|
||||
}
|
||||
}
|
||||
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
|
||||
internal bool Editable
|
||||
{
|
||||
set => textBoxName.Enabled = value;
|
||||
@@ -59,6 +63,7 @@ namespace MouseWithoutBorders
|
||||
// get { return textBoxName.Enabled; }
|
||||
}
|
||||
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
|
||||
internal bool CheckAble
|
||||
{
|
||||
set
|
||||
@@ -73,6 +78,7 @@ namespace MouseWithoutBorders
|
||||
}
|
||||
}
|
||||
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
|
||||
internal bool LocalHost
|
||||
{
|
||||
// get { return localhost; }
|
||||
@@ -165,6 +171,7 @@ namespace MouseWithoutBorders
|
||||
return rv;
|
||||
}
|
||||
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
internal SocketStatus StatusClient
|
||||
{
|
||||
get => statusClient;
|
||||
@@ -182,6 +189,7 @@ namespace MouseWithoutBorders
|
||||
}
|
||||
}
|
||||
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
internal SocketStatus StatusServer
|
||||
{
|
||||
get => statusServer;
|
||||
|
||||
@@ -12,6 +12,7 @@ using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
|
||||
// <summary>
|
||||
@@ -30,7 +31,7 @@ namespace MouseWithoutBorders.Core;
|
||||
internal static class Logger
|
||||
{
|
||||
internal static readonly string[] AllLogs = new string[MAX_LOG];
|
||||
private static readonly object AllLogsLock = new();
|
||||
private static readonly Lock AllLogsLock = new();
|
||||
internal static readonly ConcurrentDictionary<string, int> LogCounter = new();
|
||||
private const int MAX_LOG = 10000;
|
||||
private static int allLogsIndex;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
@@ -32,6 +33,7 @@ namespace MouseWithoutBorders
|
||||
|
||||
private int _animationFrame;
|
||||
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
|
||||
public bool ReturnToSettings { get; set; }
|
||||
|
||||
public SetupPage3a()
|
||||
|
||||
@@ -58,12 +58,16 @@ namespace MouseWithoutBorders
|
||||
private Timer helperTimer;
|
||||
#pragma warning restore CA2213
|
||||
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
internal int CurIcon { get; set; }
|
||||
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
internal NotifyIcon NotifyIcon { get; set; }
|
||||
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
internal System.Windows.Forms.ToolStripMenuItem MenuAllPC { get; set; }
|
||||
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
internal System.Windows.Forms.ContextMenuStrip MainMenu { get; set; }
|
||||
|
||||
internal FrmScreen()
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace MouseWithoutBorders
|
||||
public partial class FormHelper : System.Windows.Forms.Form
|
||||
{
|
||||
private readonly List<FocusArea> focusZone = new();
|
||||
private readonly object bmScreenLock = new();
|
||||
private readonly Lock bmScreenLock = new();
|
||||
private long lastClipboardEventTime;
|
||||
|
||||
private IClipboardHelper remoteClipboardHelper;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Threading;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace PowerOCR.Helpers;
|
||||
@@ -11,7 +12,7 @@ namespace PowerOCR.Helpers;
|
||||
[Export(typeof(IThrottledActionInvoker))]
|
||||
public sealed class ThrottledActionInvoker : IThrottledActionInvoker
|
||||
{
|
||||
private object _invokerLock = new object();
|
||||
private Lock _invokerLock = new Lock();
|
||||
private Action? _actionToRun;
|
||||
|
||||
private DispatcherTimer _timer;
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace PowerOCR.Settings
|
||||
private const int SettingsReadOnChangeDelayInMs = 300;
|
||||
|
||||
private readonly IFileSystemWatcher _watcher;
|
||||
private readonly object _loadingSettingsLock = new();
|
||||
private readonly Lock _loadingSettingsLock = new();
|
||||
|
||||
[ImportingConstructor]
|
||||
public UserSettings(Helpers.IThrottledActionInvoker throttledActionInvoker)
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
// 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.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Windows.Management.Deployment;
|
||||
|
||||
namespace WorkspacesCsharpLibrary.Models
|
||||
{
|
||||
public class BaseApplication : INotifyPropertyChanged, IDisposable
|
||||
{
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
public void OnPropertyChanged(PropertyChangedEventArgs e)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, e);
|
||||
}
|
||||
|
||||
public string PwaAppId { get; set; }
|
||||
|
||||
public string AppPath { get; set; }
|
||||
|
||||
private bool _isNotFound;
|
||||
|
||||
public string PackagedId { get; set; }
|
||||
|
||||
public string PackagedName { get; set; }
|
||||
|
||||
public string PackagedPublisherID { get; set; }
|
||||
|
||||
public string Aumid { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsNotFound
|
||||
{
|
||||
get
|
||||
{
|
||||
return _isNotFound;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_isNotFound != value)
|
||||
{
|
||||
_isNotFound = value;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsNotFound)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Icon _icon;
|
||||
|
||||
public Icon Icon
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_icon == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsPackagedApp)
|
||||
{
|
||||
Uri uri = GetAppLogoByPackageFamilyName();
|
||||
var bitmap = new Bitmap(uri.LocalPath);
|
||||
var iconHandle = bitmap.GetHicon();
|
||||
_icon = Icon.FromHandle(iconHandle);
|
||||
}
|
||||
else if (IsEdge || IsChrome)
|
||||
{
|
||||
string iconFilename = PwaHelper.GetPwaIconFilename(PwaAppId);
|
||||
if (!string.IsNullOrEmpty(iconFilename))
|
||||
{
|
||||
Bitmap bitmap;
|
||||
if (iconFilename.EndsWith("ico", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
bitmap = new Bitmap(iconFilename);
|
||||
}
|
||||
else
|
||||
{
|
||||
bitmap = (Bitmap)Image.FromFile(iconFilename);
|
||||
}
|
||||
|
||||
var iconHandle = bitmap.GetHicon();
|
||||
_icon = Icon.FromHandle(iconHandle);
|
||||
}
|
||||
}
|
||||
|
||||
if (_icon == null)
|
||||
{
|
||||
_icon = Icon.ExtractAssociatedIcon(AppPath);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
IsNotFound = true;
|
||||
_icon = new Icon(@"Assets\Workspaces\DefaultIcon.ico");
|
||||
}
|
||||
}
|
||||
|
||||
return _icon;
|
||||
}
|
||||
}
|
||||
|
||||
private BitmapImage _iconBitmapImage;
|
||||
|
||||
public BitmapImage IconBitmapImage
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_iconBitmapImage == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Bitmap previewBitmap = new Bitmap(32, 32);
|
||||
using (Graphics graphics = Graphics.FromImage(previewBitmap))
|
||||
{
|
||||
graphics.SmoothingMode = SmoothingMode.AntiAlias;
|
||||
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
||||
|
||||
graphics.DrawIcon(Icon, new Rectangle(0, 0, 32, 32));
|
||||
}
|
||||
|
||||
using (var memory = new MemoryStream())
|
||||
{
|
||||
previewBitmap.Save(memory, ImageFormat.Png);
|
||||
memory.Position = 0;
|
||||
|
||||
BitmapImage bitmapImage = new BitmapImage();
|
||||
bitmapImage.BeginInit();
|
||||
bitmapImage.StreamSource = memory;
|
||||
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
|
||||
bitmapImage.EndInit();
|
||||
bitmapImage.Freeze();
|
||||
|
||||
_iconBitmapImage = bitmapImage;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return _iconBitmapImage;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsEdge
|
||||
{
|
||||
get => AppPath.EndsWith("edge.exe", StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
public bool IsChrome
|
||||
{
|
||||
get => AppPath.EndsWith("chrome.exe", StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
public Uri GetAppLogoByPackageFamilyName()
|
||||
{
|
||||
var pkgManager = new PackageManager();
|
||||
var pkg = pkgManager.FindPackagesForUser(string.Empty, PackagedId).FirstOrDefault();
|
||||
|
||||
if (pkg == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return pkg.Logo;
|
||||
}
|
||||
|
||||
private bool? _isPackagedApp;
|
||||
|
||||
public bool IsPackagedApp
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_isPackagedApp == null)
|
||||
{
|
||||
if (!AppPath.StartsWith("C:\\Program Files\\WindowsApps\\", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
_isPackagedApp = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
string appPath = AppPath.Replace("C:\\Program Files\\WindowsApps\\", string.Empty);
|
||||
Regex packagedAppPathRegex = new Regex(@"(?<APPID>[^_]*)_\d+.\d+.\d+.\d+_x64__(?<PublisherID>[^\\]*)", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
||||
Match match = packagedAppPathRegex.Match(appPath);
|
||||
_isPackagedApp = match.Success;
|
||||
if (match.Success)
|
||||
{
|
||||
PackagedName = match.Groups["APPID"].Value;
|
||||
PackagedPublisherID = match.Groups["PublisherID"].Value;
|
||||
PackagedId = $"{PackagedName}_{PackagedPublisherID}";
|
||||
Aumid = $"{PackagedId}!App";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _isPackagedApp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/modules/Workspaces/WorkspacesCsharpLibrary/PwaApp.cs
Normal file
15
src/modules/Workspaces/WorkspacesCsharpLibrary/PwaApp.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
// 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 WorkspacesCsharpLibrary
|
||||
{
|
||||
public class PwaApp
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
|
||||
public required string IconFilename { get; set; }
|
||||
|
||||
public required string AppId { get; set; }
|
||||
}
|
||||
}
|
||||
90
src/modules/Workspaces/WorkspacesCsharpLibrary/PwaHelper.cs
Normal file
90
src/modules/Workspaces/WorkspacesCsharpLibrary/PwaHelper.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
// 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.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace WorkspacesCsharpLibrary
|
||||
{
|
||||
public class PwaHelper
|
||||
{
|
||||
private const string ChromeBase = "Google\\Chrome\\User Data\\Default\\Web Applications";
|
||||
private const string EdgeBase = "Microsoft\\Edge\\User Data\\Default\\Web Applications";
|
||||
private const string ResourcesDir = "Manifest Resources";
|
||||
private const string IconsDir = "Icons";
|
||||
private const string PwaDirIdentifier = "_CRX_";
|
||||
|
||||
private static List<PwaApp> pwaApps = new List<PwaApp>();
|
||||
|
||||
public PwaHelper()
|
||||
{
|
||||
InitPwaData(EdgeBase);
|
||||
InitPwaData(ChromeBase);
|
||||
}
|
||||
|
||||
private void InitPwaData(string p_baseDir)
|
||||
{
|
||||
var baseFolderName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), p_baseDir);
|
||||
if (Directory.Exists(baseFolderName))
|
||||
{
|
||||
foreach (string subDir in Directory.GetDirectories(baseFolderName))
|
||||
{
|
||||
string dirName = Path.GetFileName(subDir);
|
||||
if (!dirName.StartsWith(PwaDirIdentifier, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string appId = dirName.Substring(PwaDirIdentifier.Length, dirName.Length - PwaDirIdentifier.Length).Trim('_');
|
||||
|
||||
foreach (string iconFile in Directory.GetFiles(subDir, "*.ico"))
|
||||
{
|
||||
string filenameWithoutExtension = Path.GetFileNameWithoutExtension(iconFile);
|
||||
|
||||
pwaApps.Add(new PwaApp() { Name = filenameWithoutExtension, IconFilename = iconFile, AppId = appId });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
string resourcesDir = Path.Combine(baseFolderName, ResourcesDir);
|
||||
if (Directory.Exists(resourcesDir))
|
||||
{
|
||||
foreach (string subDir in Directory.GetDirectories(resourcesDir))
|
||||
{
|
||||
string dirName = Path.GetFileName(subDir);
|
||||
if (pwaApps.Any(app => app.AppId == dirName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string iconsDir = Path.Combine(subDir, IconsDir);
|
||||
if (Directory.Exists(iconsDir))
|
||||
{
|
||||
foreach (string iconFile in Directory.GetFiles(iconsDir, "*.png"))
|
||||
{
|
||||
string filenameWithoutExtension = Path.GetFileNameWithoutExtension(iconFile);
|
||||
|
||||
pwaApps.Add(new PwaApp() { Name = filenameWithoutExtension, IconFilename = iconFile, AppId = dirName });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetPwaIconFilename(string pwaAppId)
|
||||
{
|
||||
var candidates = pwaApps.Where(x => x.AppId == pwaAppId).ToList();
|
||||
if (candidates.Count > 0)
|
||||
{
|
||||
return candidates.First().IconFilename;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyTitle>PowerToys.WorkspacesCsharpLibrary</AssemblyTitle>
|
||||
<AssemblyDescription>PowerToys Workspaces Csharp Library</AssemblyDescription>
|
||||
<Description>PowerToys Workspaces Csharp Library</Description>
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
|
||||
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)</OutputPath>
|
||||
<AssemblyName>PowerToys.WorkspacesCsharpLibrary</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -37,6 +37,8 @@ namespace WorkspacesEditor.Data
|
||||
|
||||
public string AppUserModelId { get; set; }
|
||||
|
||||
public string PwaAppId { get; set; }
|
||||
|
||||
public string CommandLineArguments { get; set; }
|
||||
|
||||
public bool IsElevated { get; set; }
|
||||
|
||||
@@ -13,18 +13,17 @@ using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
using ManagedCommon;
|
||||
using Windows.Management.Deployment;
|
||||
using WorkspacesCsharpLibrary;
|
||||
using WorkspacesCsharpLibrary.Models;
|
||||
|
||||
namespace WorkspacesEditor.Models
|
||||
{
|
||||
public class Application : INotifyPropertyChanged, IDisposable
|
||||
public class Application : BaseApplication, IDisposable
|
||||
{
|
||||
private bool _isInitialized;
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
public Application()
|
||||
{
|
||||
}
|
||||
@@ -37,6 +36,7 @@ namespace WorkspacesEditor.Models
|
||||
AppTitle = other.AppTitle;
|
||||
PackageFullName = other.PackageFullName;
|
||||
AppUserModelId = other.AppUserModelId;
|
||||
PwaAppId = other.PwaAppId;
|
||||
CommandLineArguments = other.CommandLineArguments;
|
||||
IsElevated = other.IsElevated;
|
||||
CanLaunchElevated = other.CanLaunchElevated;
|
||||
@@ -100,8 +100,6 @@ namespace WorkspacesEditor.Models
|
||||
|
||||
public string AppName { get; set; }
|
||||
|
||||
public string AppPath { get; set; }
|
||||
|
||||
public string AppTitle { get; set; }
|
||||
|
||||
public string PackageFullName { get; set; }
|
||||
@@ -187,26 +185,6 @@ namespace WorkspacesEditor.Models
|
||||
|
||||
public bool IsAppMainParamVisible { get => !string.IsNullOrWhiteSpace(_appMainParams); }
|
||||
|
||||
private bool _isNotFound;
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsNotFound
|
||||
{
|
||||
get
|
||||
{
|
||||
return _isNotFound;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_isNotFound != value)
|
||||
{
|
||||
_isNotFound = value;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsNotFound)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsHighlighted { get; set; }
|
||||
|
||||
@@ -222,100 +200,6 @@ namespace WorkspacesEditor.Models
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
private Icon _icon = null;
|
||||
|
||||
[JsonIgnore]
|
||||
public Icon Icon
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_icon == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsPackagedApp)
|
||||
{
|
||||
Uri uri = GetAppLogoByPackageFamilyName();
|
||||
var bitmap = new Bitmap(uri.LocalPath);
|
||||
var iconHandle = bitmap.GetHicon();
|
||||
_icon = Icon.FromHandle(iconHandle);
|
||||
}
|
||||
else
|
||||
{
|
||||
_icon = Icon.ExtractAssociatedIcon(AppPath);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Logger.LogWarning($"Icon not found on app path: {AppPath}. Using default icon");
|
||||
IsNotFound = true;
|
||||
_icon = new Icon(@"Assets\Workspaces\DefaultIcon.ico");
|
||||
}
|
||||
}
|
||||
|
||||
return _icon;
|
||||
}
|
||||
}
|
||||
|
||||
public Uri GetAppLogoByPackageFamilyName()
|
||||
{
|
||||
var pkgManager = new PackageManager();
|
||||
var pkg = pkgManager.FindPackagesForUser(string.Empty, PackagedId).FirstOrDefault();
|
||||
|
||||
if (pkg == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return pkg.Logo;
|
||||
}
|
||||
|
||||
private BitmapImage _iconBitmapImage;
|
||||
|
||||
public BitmapImage IconBitmapImage
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_iconBitmapImage == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Bitmap previewBitmap = new Bitmap(32, 32);
|
||||
using (Graphics graphics = Graphics.FromImage(previewBitmap))
|
||||
{
|
||||
graphics.SmoothingMode = SmoothingMode.AntiAlias;
|
||||
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
||||
|
||||
graphics.DrawIcon(Icon, new Rectangle(0, 0, 32, 32));
|
||||
}
|
||||
|
||||
using (var memory = new MemoryStream())
|
||||
{
|
||||
previewBitmap.Save(memory, ImageFormat.Png);
|
||||
memory.Position = 0;
|
||||
|
||||
var bitmapImage = new BitmapImage();
|
||||
bitmapImage.BeginInit();
|
||||
bitmapImage.StreamSource = memory;
|
||||
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
|
||||
bitmapImage.EndInit();
|
||||
bitmapImage.Freeze();
|
||||
|
||||
_iconBitmapImage = bitmapImage;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError($"Exception while drawing icon for app with path: {AppPath}. Exception message: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return _iconBitmapImage;
|
||||
}
|
||||
}
|
||||
|
||||
private WindowPosition _position;
|
||||
|
||||
public WindowPosition Position
|
||||
@@ -367,56 +251,11 @@ namespace WorkspacesEditor.Models
|
||||
}
|
||||
}
|
||||
|
||||
public void OnPropertyChanged(PropertyChangedEventArgs e)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, e);
|
||||
}
|
||||
|
||||
public void InitializationFinished()
|
||||
{
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
private bool? _isPackagedApp;
|
||||
|
||||
public string PackagedId { get; set; }
|
||||
|
||||
public string PackagedName { get; set; }
|
||||
|
||||
public string PackagedPublisherID { get; set; }
|
||||
|
||||
public string Aumid { get; set; }
|
||||
|
||||
public bool IsPackagedApp
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_isPackagedApp == null)
|
||||
{
|
||||
if (!AppPath.StartsWith("C:\\Program Files\\WindowsApps\\", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
_isPackagedApp = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
string appPath = AppPath.Replace("C:\\Program Files\\WindowsApps\\", string.Empty);
|
||||
Regex packagedAppPathRegex = new Regex(@"(?<APPID>[^_]*)_\d+.\d+.\d+.\d+_x64__(?<PublisherID>[^\\]*)", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
||||
Match match = packagedAppPathRegex.Match(appPath);
|
||||
_isPackagedApp = match.Success;
|
||||
if (match.Success)
|
||||
{
|
||||
PackagedName = match.Groups["APPID"].Value;
|
||||
PackagedPublisherID = match.Groups["PublisherID"].Value;
|
||||
PackagedId = $"{PackagedName}_{PackagedPublisherID}";
|
||||
Aumid = $"{PackagedId}!App";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _isPackagedApp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private bool _isExpanded;
|
||||
|
||||
public bool IsExpanded
|
||||
@@ -454,11 +293,6 @@ namespace WorkspacesEditor.Models
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
internal void CommandLineTextChanged(string newCommandLineValue)
|
||||
{
|
||||
CommandLineArguments = newCommandLineValue;
|
||||
|
||||
@@ -251,10 +251,11 @@ namespace WorkspacesEditor.Models
|
||||
{
|
||||
Models.Application newApp = new Models.Application()
|
||||
{
|
||||
Id = app.Id != null ? app.Id : $"{{{Guid.NewGuid().ToString()}}}",
|
||||
Id = string.IsNullOrEmpty(app.Id) ? $"{{{Guid.NewGuid().ToString()}}}" : app.Id,
|
||||
AppName = app.Application,
|
||||
AppPath = app.ApplicationPath,
|
||||
AppTitle = app.Title,
|
||||
PwaAppId = string.IsNullOrEmpty(app.PwaAppId) ? string.Empty : app.PwaAppId,
|
||||
PackageFullName = app.PackageFullName,
|
||||
AppUserModelId = app.AppUserModelId,
|
||||
Parent = this,
|
||||
|
||||
@@ -75,16 +75,16 @@ namespace WorkspacesEditor.Utils
|
||||
|
||||
foreach (Application app in appsIncluded)
|
||||
{
|
||||
if (repeatCounter.TryGetValue(app.AppPath, out int value))
|
||||
if (repeatCounter.TryGetValue(app.AppPath + app.AppTitle, out int value))
|
||||
{
|
||||
repeatCounter[app.AppPath] = ++value;
|
||||
repeatCounter[app.AppPath + app.AppTitle] = ++value;
|
||||
}
|
||||
else
|
||||
{
|
||||
repeatCounter.Add(app.AppPath, 1);
|
||||
repeatCounter.Add(app.AppPath + app.AppTitle, 1);
|
||||
}
|
||||
|
||||
app.RepeatIndex = repeatCounter[app.AppPath];
|
||||
app.RepeatIndex = repeatCounter[app.AppPath + app.AppTitle];
|
||||
}
|
||||
|
||||
foreach (Application app in project.Applications.Where(x => !x.IsIncluded))
|
||||
|
||||
@@ -103,6 +103,7 @@ namespace WorkspacesEditor.Utils
|
||||
Title = app.AppTitle,
|
||||
PackageFullName = app.PackageFullName,
|
||||
AppUserModelId = app.AppUserModelId,
|
||||
PwaAppId = app.PwaAppId,
|
||||
CommandLineArguments = app.CommandLineArguments,
|
||||
IsElevated = app.IsElevated,
|
||||
CanLaunchElevated = app.CanLaunchElevated,
|
||||
|
||||
@@ -14,10 +14,10 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Timers;
|
||||
using System.Windows;
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using WorkspacesCsharpLibrary;
|
||||
using WorkspacesEditor.Data;
|
||||
using WorkspacesEditor.Models;
|
||||
using WorkspacesEditor.Telemetry;
|
||||
@@ -39,6 +39,7 @@ namespace WorkspacesEditor.ViewModels
|
||||
private MainWindow _mainWindow;
|
||||
private Timer lastUpdatedTimer;
|
||||
private WorkspacesSettings settings;
|
||||
private PwaHelper _pwaHelper;
|
||||
|
||||
public ObservableCollection<Project> Workspaces { get; set; } = new ObservableCollection<Project>();
|
||||
|
||||
@@ -147,6 +148,7 @@ namespace WorkspacesEditor.ViewModels
|
||||
settings = Utils.Settings.ReadSettings();
|
||||
_orderByIndex = (int)settings.Properties.SortBy;
|
||||
_workspacesEditorIO = workspacesEditorIO;
|
||||
_pwaHelper = new PwaHelper();
|
||||
lastUpdatedTimer = new System.Timers.Timer();
|
||||
lastUpdatedTimer.Interval = 1000;
|
||||
lastUpdatedTimer.Elapsed += LastUpdatedTimerElapsed;
|
||||
|
||||
@@ -65,7 +65,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ControlzEx" />
|
||||
<PackageReference Include="ModernWpfUI" />
|
||||
<PackageReference Include="System.IO.Abstractions" />
|
||||
</ItemGroup>
|
||||
@@ -77,6 +76,7 @@
|
||||
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
|
||||
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
|
||||
<ProjectReference Include="..\WorkspacesCsharpLibrary\WorkspacesCsharpLibrary.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
Margin="10"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Source="{Binding IconBitmapImage}" />
|
||||
Source="{Binding IconBitmapImage, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
Width="20"
|
||||
|
||||
@@ -18,6 +18,15 @@ using namespace Windows::Management::Deployment;
|
||||
|
||||
namespace AppLauncher
|
||||
{
|
||||
namespace NonLocalizable
|
||||
{
|
||||
const std::wstring EdgeFilename = L"msedge.exe";
|
||||
const std::wstring EdgePwaFilename = L"msedge_proxy.exe";
|
||||
const std::wstring ChromeFilename = L"chrome.exe";
|
||||
const std::wstring ChromePwaFilename = L"chrome_proxy.exe";
|
||||
const std::wstring PwaCommandLineAddition = L"--profile-directory=Default --app-id=";
|
||||
}
|
||||
|
||||
Result<SHELLEXECUTEINFO, std::wstring> LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated)
|
||||
{
|
||||
std::wstring dir = std::filesystem::path(appPath).parent_path();
|
||||
@@ -133,30 +142,50 @@ namespace AppLauncher
|
||||
launched = LaunchPackagedApp(app.packageFullName, launchErrors);
|
||||
}
|
||||
|
||||
std::wstring appPathFinal;
|
||||
std::wstring commandLineArgsFinal;
|
||||
appPathFinal = app.path;
|
||||
commandLineArgsFinal = app.commandLineArgs;
|
||||
|
||||
if (!launched && !app.pwaAppId.empty())
|
||||
{
|
||||
std::filesystem::path appPath(app.path);
|
||||
if (appPath.filename() == NonLocalizable::EdgeFilename)
|
||||
{
|
||||
appPathFinal = appPath.parent_path() / NonLocalizable::EdgePwaFilename;
|
||||
commandLineArgsFinal = NonLocalizable::PwaCommandLineAddition + app.pwaAppId + L" " + app.commandLineArgs;
|
||||
}
|
||||
if (appPath.filename() == NonLocalizable::ChromeFilename)
|
||||
{
|
||||
appPathFinal = appPath.parent_path() / NonLocalizable::ChromePwaFilename;
|
||||
commandLineArgsFinal = NonLocalizable::PwaCommandLineAddition + app.pwaAppId + L" " + app.commandLineArgs;
|
||||
}
|
||||
}
|
||||
|
||||
if (!launched)
|
||||
{
|
||||
Logger::trace(L"Launching {} at {}", app.name, app.path);
|
||||
Logger::trace(L"Launching {} at {}", app.name, appPathFinal);
|
||||
|
||||
DWORD dwAttrib = GetFileAttributesW(app.path.c_str());
|
||||
DWORD dwAttrib = GetFileAttributesW(appPathFinal.c_str());
|
||||
if (dwAttrib == INVALID_FILE_ATTRIBUTES)
|
||||
{
|
||||
Logger::error(L"File not found at {}", app.path);
|
||||
launchErrors.push_back({ std::filesystem::path(app.path).filename(), L"File not found" });
|
||||
Logger::error(L"File not found at {}", appPathFinal);
|
||||
launchErrors.push_back({ std::filesystem::path(appPathFinal).filename(), L"File not found" });
|
||||
return false;
|
||||
}
|
||||
|
||||
auto res = LaunchApp(app.path, app.commandLineArgs, app.isElevated);
|
||||
auto res = LaunchApp(appPathFinal, commandLineArgsFinal, app.isElevated);
|
||||
if (res.isOk())
|
||||
{
|
||||
launched = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
launchErrors.push_back({ std::filesystem::path(app.path).filename(), res.error() });
|
||||
launchErrors.push_back({ std::filesystem::path(appPathFinal).filename(), res.error() });
|
||||
}
|
||||
}
|
||||
|
||||
Logger::trace(L"{} {} at {}", app.name, (launched ? L"launched" : L"not launched"), app.path);
|
||||
Logger::trace(L"{} {} at {}", app.name, (launched ? L"launched" : L"not launched"), appPathFinal);
|
||||
return launched;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@ namespace WorkspacesLauncherUI.Data
|
||||
|
||||
public string AppUserModelId { get; set; }
|
||||
|
||||
public string PwaAppId { get; set; }
|
||||
|
||||
public string CommandLineArguments { get; set; }
|
||||
|
||||
public bool IsElevated { get; set; }
|
||||
|
||||
@@ -3,77 +3,23 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
using ManagedCommon;
|
||||
using Windows.Management.Deployment;
|
||||
using WorkspacesCsharpLibrary.Models;
|
||||
using WorkspacesLauncherUI.Data;
|
||||
|
||||
namespace WorkspacesLauncherUI.Models
|
||||
{
|
||||
public class AppLaunching : INotifyPropertyChanged, IDisposable
|
||||
public class AppLaunching : BaseApplication, IDisposable
|
||||
{
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
public void OnPropertyChanged(PropertyChangedEventArgs e)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, e);
|
||||
}
|
||||
|
||||
public ApplicationWrapper Application { get; set; }
|
||||
|
||||
public bool Loading => LaunchState == LaunchingState.Waiting || LaunchState == LaunchingState.Launched;
|
||||
|
||||
private Icon _icon;
|
||||
|
||||
public Icon Icon
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_icon == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsPackagedApp)
|
||||
{
|
||||
Uri uri = GetAppLogoByPackageFamilyName();
|
||||
var bitmap = new Bitmap(uri.LocalPath);
|
||||
var iconHandle = bitmap.GetHicon();
|
||||
_icon = Icon.FromHandle(iconHandle);
|
||||
}
|
||||
else
|
||||
{
|
||||
_icon = Icon.ExtractAssociatedIcon(Application.ApplicationPath);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Logger.LogWarning($"Icon not found on app path: {Application.ApplicationPath}. Using default icon");
|
||||
IsNotFound = true;
|
||||
_icon = new Icon(@"Assets\Workspaces\DefaultIcon.ico");
|
||||
}
|
||||
}
|
||||
|
||||
return _icon;
|
||||
}
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
return Application.Application;
|
||||
}
|
||||
}
|
||||
public string Name { get; set; }
|
||||
|
||||
public LaunchingState LaunchState { get; set; }
|
||||
|
||||
@@ -96,128 +42,5 @@ namespace WorkspacesLauncherUI.Models
|
||||
_ => new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 254, 0, 0)),
|
||||
};
|
||||
}
|
||||
|
||||
private bool _isNotFound;
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsNotFound
|
||||
{
|
||||
get
|
||||
{
|
||||
return _isNotFound;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_isNotFound != value)
|
||||
{
|
||||
_isNotFound = value;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsNotFound)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Uri GetAppLogoByPackageFamilyName()
|
||||
{
|
||||
var pkgManager = new PackageManager();
|
||||
var pkg = pkgManager.FindPackagesForUser(string.Empty, PackagedId).FirstOrDefault();
|
||||
|
||||
if (pkg == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return pkg.Logo;
|
||||
}
|
||||
|
||||
private bool? _isPackagedApp;
|
||||
|
||||
public string PackagedId { get; set; }
|
||||
|
||||
public string PackagedName { get; set; }
|
||||
|
||||
public string PackagedPublisherID { get; set; }
|
||||
|
||||
public string Aumid { get; set; }
|
||||
|
||||
public bool IsPackagedApp
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_isPackagedApp == null)
|
||||
{
|
||||
if (!Application.ApplicationPath.StartsWith("C:\\Program Files\\WindowsApps\\", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
_isPackagedApp = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
string appPath = Application.ApplicationPath.Replace("C:\\Program Files\\WindowsApps\\", string.Empty);
|
||||
Regex packagedAppPathRegex = new Regex(@"(?<APPID>[^_]*)_\d+.\d+.\d+.\d+_x64__(?<PublisherID>[^\\]*)", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
||||
Match match = packagedAppPathRegex.Match(appPath);
|
||||
_isPackagedApp = match.Success;
|
||||
if (match.Success)
|
||||
{
|
||||
PackagedName = match.Groups["APPID"].Value;
|
||||
PackagedPublisherID = match.Groups["PublisherID"].Value;
|
||||
PackagedId = $"{PackagedName}_{PackagedPublisherID}";
|
||||
Aumid = $"{PackagedId}!App";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _isPackagedApp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private BitmapImage _iconBitmapImage;
|
||||
|
||||
public BitmapImage IconBitmapImage
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_iconBitmapImage == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Bitmap previewBitmap = new Bitmap(32, 32);
|
||||
using (Graphics graphics = Graphics.FromImage(previewBitmap))
|
||||
{
|
||||
graphics.SmoothingMode = SmoothingMode.AntiAlias;
|
||||
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
||||
|
||||
graphics.DrawIcon(Icon, new Rectangle(0, 0, 32, 32));
|
||||
}
|
||||
|
||||
using (var memory = new MemoryStream())
|
||||
{
|
||||
previewBitmap.Save(memory, ImageFormat.Png);
|
||||
memory.Position = 0;
|
||||
|
||||
var bitmapImage = new BitmapImage();
|
||||
bitmapImage.BeginInit();
|
||||
bitmapImage.StreamSource = memory;
|
||||
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
|
||||
bitmapImage.EndInit();
|
||||
bitmapImage.Freeze();
|
||||
|
||||
_iconBitmapImage = bitmapImage;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError($"Exception while drawing icon for app with path: {Application.ApplicationPath}. Exception message: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return _iconBitmapImage;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,10 @@ using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
|
||||
using ManagedCommon;
|
||||
using WorkspacesCsharpLibrary;
|
||||
using WorkspacesLauncherUI.Data;
|
||||
using WorkspacesLauncherUI.Models;
|
||||
using WorkspacesLauncherUI.Utils;
|
||||
|
||||
namespace WorkspacesLauncherUI.ViewModels
|
||||
{
|
||||
@@ -20,6 +22,7 @@ namespace WorkspacesLauncherUI.ViewModels
|
||||
|
||||
private StatusWindow _snapshotWindow;
|
||||
private int launcherProcessID;
|
||||
private PwaHelper _pwaHelper;
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
@@ -30,6 +33,8 @@ namespace WorkspacesLauncherUI.ViewModels
|
||||
|
||||
public MainViewModel()
|
||||
{
|
||||
_pwaHelper = new PwaHelper();
|
||||
|
||||
// receive IPC Message
|
||||
App.IPCMessageReceivedCallback = (string msg) =>
|
||||
{
|
||||
@@ -54,7 +59,11 @@ namespace WorkspacesLauncherUI.ViewModels
|
||||
{
|
||||
appLaunchingList.Add(new AppLaunching()
|
||||
{
|
||||
Application = app.Application,
|
||||
Name = app.Application.Application,
|
||||
AppPath = app.Application.ApplicationPath,
|
||||
PackagedName = app.Application.PackageFullName,
|
||||
Aumid = app.Application.AppUserModelId,
|
||||
PwaAppId = app.Application.PwaAppId,
|
||||
LaunchState = app.State,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
|
||||
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
|
||||
<ProjectReference Include="..\WorkspacesCsharpLibrary\WorkspacesCsharpLibrary.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace WorkspacesData
|
||||
std::wstring settingsFolderPath = PTSettingsHelper::get_module_save_folder_location(NonLocalizable::ModuleKey);
|
||||
return settingsFolderPath + L"\\temp-workspaces.json";
|
||||
}
|
||||
|
||||
|
||||
RECT WorkspacesProject::Application::Position::toRect() const noexcept
|
||||
{
|
||||
return RECT{ .left = x, .top = y, .right = x + width, .bottom = y + height };
|
||||
@@ -79,6 +79,7 @@ namespace WorkspacesData
|
||||
const static wchar_t* AppPathID = L"application-path";
|
||||
const static wchar_t* AppPackageFullNameID = L"package-full-name";
|
||||
const static wchar_t* AppUserModelId = L"app-user-model-id";
|
||||
const static wchar_t* PwaAppId = L"pwa-app-id";
|
||||
const static wchar_t* AppTitleID = L"title";
|
||||
const static wchar_t* CommandLineArgsID = L"command-line-arguments";
|
||||
const static wchar_t* ElevatedID = L"is-elevated";
|
||||
@@ -98,6 +99,7 @@ namespace WorkspacesData
|
||||
json.SetNamedValue(NonLocalizable::AppTitleID, json::value(data.title));
|
||||
json.SetNamedValue(NonLocalizable::AppPackageFullNameID, json::value(data.packageFullName));
|
||||
json.SetNamedValue(NonLocalizable::AppUserModelId, json::value(data.appUserModelId));
|
||||
json.SetNamedValue(NonLocalizable::PwaAppId, json::value(data.pwaAppId));
|
||||
json.SetNamedValue(NonLocalizable::CommandLineArgsID, json::value(data.commandLineArgs));
|
||||
json.SetNamedValue(NonLocalizable::ElevatedID, json::value(data.isElevated));
|
||||
json.SetNamedValue(NonLocalizable::CanLaunchElevatedID, json::value(data.canLaunchElevated));
|
||||
@@ -136,6 +138,11 @@ namespace WorkspacesData
|
||||
result.appUserModelId = json.GetNamedString(NonLocalizable::AppUserModelId);
|
||||
}
|
||||
|
||||
if (json.HasKey(NonLocalizable::PwaAppId))
|
||||
{
|
||||
result.pwaAppId = json.GetNamedString(NonLocalizable::PwaAppId);
|
||||
}
|
||||
|
||||
result.commandLineArgs = json.GetNamedString(NonLocalizable::CommandLineArgsID);
|
||||
|
||||
if (json.HasKey(NonLocalizable::ElevatedID))
|
||||
@@ -330,11 +337,11 @@ namespace WorkspacesData
|
||||
{
|
||||
result.isShortcutNeeded = json.GetNamedBoolean(NonLocalizable::IsShortcutNeededID);
|
||||
}
|
||||
|
||||
|
||||
if (json.HasKey(NonLocalizable::MoveExistingWindowsID))
|
||||
{
|
||||
result.moveExistingWindows = json.GetNamedBoolean(NonLocalizable::MoveExistingWindowsID);
|
||||
}
|
||||
result.moveExistingWindows = json.GetNamedBoolean(NonLocalizable::MoveExistingWindowsID);
|
||||
}
|
||||
|
||||
auto appsArray = json.GetNamedArray(NonLocalizable::AppsID);
|
||||
for (uint32_t i = 0; i < appsArray.Size(); ++i)
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace WorkspacesData
|
||||
std::wstring path;
|
||||
std::wstring packageFullName;
|
||||
std::wstring appUserModelId;
|
||||
std::wstring pwaAppId;
|
||||
std::wstring commandLineArgs;
|
||||
bool isElevated{};
|
||||
bool canLaunchElevated{};
|
||||
|
||||
409
src/modules/Workspaces/WorkspacesSnapshotTool/PwaHelper.cpp
Normal file
409
src/modules/Workspaces/WorkspacesSnapshotTool/PwaHelper.cpp
Normal file
@@ -0,0 +1,409 @@
|
||||
#include "pch.h"
|
||||
#include "PwaHelper.h"
|
||||
#include <ShlObj.h>
|
||||
#include <tlhelp32.h>
|
||||
#include <winternl.h>
|
||||
#include <initguid.h>
|
||||
#include <filesystem>
|
||||
#include <wil/result_macros.h>
|
||||
#include <common/logger/logger.h>
|
||||
#include <common/utils/winapi_error.h>
|
||||
#include <wil\com.h>
|
||||
#pragma comment(lib, "ntdll.lib")
|
||||
|
||||
namespace SnapshotUtils
|
||||
{
|
||||
namespace NonLocalizable
|
||||
{
|
||||
const std::wstring EdgeAppIdIdentifier = L"--app-id=";
|
||||
const std::wstring ChromeAppIdIdentifier = L"Chrome._crx_";
|
||||
const std::wstring ChromeBase = L"Google\\Chrome\\User Data\\Default\\Web Applications";
|
||||
const std::wstring EdgeBase = L"Microsoft\\Edge\\User Data\\Default\\Web Applications";
|
||||
const std::wstring ChromeDirPrefix = L"_crx_";
|
||||
const std::wstring EdgeDirPrefix = L"_crx__";
|
||||
}
|
||||
// {c8900b66-a973-584b-8cae-355b7f55341b}
|
||||
DEFINE_GUID(CLSID_StartMenuCacheAndAppResolver, 0x660b90c8, 0x73a9, 0x4b58, 0x8c, 0xae, 0x35, 0x5b, 0x7f, 0x55, 0x34, 0x1b);
|
||||
|
||||
// {46a6eeff-908e-4dc6-92a6-64be9177b41c}
|
||||
DEFINE_GUID(IID_IAppResolver_7, 0x46a6eeff, 0x908e, 0x4dc6, 0x92, 0xa6, 0x64, 0xbe, 0x91, 0x77, 0xb4, 0x1c);
|
||||
|
||||
// {de25675a-72de-44b4-9373-05170450c140}
|
||||
DEFINE_GUID(IID_IAppResolver_8, 0xde25675a, 0x72de, 0x44b4, 0x93, 0x73, 0x05, 0x17, 0x04, 0x50, 0xc1, 0x40);
|
||||
|
||||
struct IAppResolver_7 : public IUnknown
|
||||
{
|
||||
public:
|
||||
virtual HRESULT STDMETHODCALLTYPE GetAppIDForShortcut() = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE GetAppIDForWindow(HWND hWnd, WCHAR** pszAppId, void* pUnknown1, void* pUnknown2, void* pUnknown3) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE GetAppIDForProcess(DWORD dwProcessId, WCHAR** pszAppId, void* pUnknown1, void* pUnknown2, void* pUnknown3) = 0;
|
||||
};
|
||||
|
||||
struct IAppResolver_8 : public IUnknown
|
||||
{
|
||||
public:
|
||||
virtual HRESULT STDMETHODCALLTYPE GetAppIDForShortcut() = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE GetAppIDForShortcutObject() = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE GetAppIDForWindow(HWND hWnd, WCHAR** pszAppId, void* pUnknown1, void* pUnknown2, void* pUnknown3) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE GetAppIDForProcess(DWORD dwProcessId, WCHAR** pszAppId, void* pUnknown1, void* pUnknown2, void* pUnknown3) = 0;
|
||||
};
|
||||
|
||||
BOOL GetAppId_7(HWND hWnd, std::wstring* result)
|
||||
{
|
||||
HRESULT hr;
|
||||
|
||||
wil::com_ptr<IAppResolver_7> appResolver;
|
||||
hr = CoCreateInstance(CLSID_StartMenuCacheAndAppResolver, NULL, CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER, IID_IAppResolver_7, reinterpret_cast<void**>(appResolver.put()));
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
wil::unique_cotaskmem_string pszAppId;
|
||||
hr = appResolver->GetAppIDForWindow(hWnd, &pszAppId, NULL, NULL, NULL);
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
*result = std::wstring(pszAppId.get());
|
||||
}
|
||||
|
||||
appResolver->Release();
|
||||
}
|
||||
|
||||
return SUCCEEDED(hr);
|
||||
}
|
||||
|
||||
BOOL GetAppId_8(HWND hWnd, std::wstring* result)
|
||||
{
|
||||
HRESULT hr;
|
||||
*result = L"";
|
||||
|
||||
wil::com_ptr<IAppResolver_8> appResolver;
|
||||
hr = CoCreateInstance(CLSID_StartMenuCacheAndAppResolver, NULL, CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER, IID_IAppResolver_8, reinterpret_cast<void**>(appResolver.put()));
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
wil::unique_cotaskmem_string pszAppId;
|
||||
hr = appResolver->GetAppIDForWindow(hWnd, &pszAppId, NULL, NULL, NULL);
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
*result = std::wstring(pszAppId.get());
|
||||
}
|
||||
|
||||
appResolver->Release();
|
||||
}
|
||||
|
||||
return SUCCEEDED(hr);
|
||||
}
|
||||
|
||||
BOOL PwaHelper::GetAppId(HWND hWnd, std::wstring* result)
|
||||
{
|
||||
HRESULT hr = GetAppId_8(hWnd, result);
|
||||
if (!SUCCEEDED(hr))
|
||||
{
|
||||
hr = GetAppId_7(hWnd, result);
|
||||
}
|
||||
return SUCCEEDED(hr);
|
||||
}
|
||||
|
||||
BOOL GetProcessId_7(DWORD dwProcessId, std::wstring* result)
|
||||
{
|
||||
HRESULT hr;
|
||||
*result = L"";
|
||||
|
||||
wil::com_ptr<IAppResolver_7> appResolver;
|
||||
hr = CoCreateInstance(CLSID_StartMenuCacheAndAppResolver, NULL, CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER, IID_IAppResolver_7, reinterpret_cast<void**>(appResolver.put()));
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
wil::unique_cotaskmem_string pszAppId;
|
||||
hr = appResolver->GetAppIDForProcess(dwProcessId, &pszAppId, NULL, NULL, NULL);
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
*result = std::wstring(pszAppId.get());
|
||||
}
|
||||
|
||||
appResolver->Release();
|
||||
}
|
||||
|
||||
return SUCCEEDED(hr);
|
||||
}
|
||||
|
||||
BOOL GetProcessId_8(DWORD dwProcessId, std::wstring* result)
|
||||
{
|
||||
HRESULT hr;
|
||||
*result = L"";
|
||||
|
||||
wil::com_ptr<IAppResolver_8> appResolver;
|
||||
hr = CoCreateInstance(CLSID_StartMenuCacheAndAppResolver, NULL, CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER, IID_IAppResolver_8, reinterpret_cast<void**>(appResolver.put()));
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
wil::unique_cotaskmem_string pszAppId;
|
||||
hr = appResolver->GetAppIDForProcess(dwProcessId, &pszAppId, NULL, NULL, NULL);
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
*result = std::wstring(pszAppId.get());
|
||||
}
|
||||
|
||||
appResolver->Release();
|
||||
}
|
||||
|
||||
return SUCCEEDED(hr);
|
||||
}
|
||||
|
||||
BOOL GetProcessId(DWORD dwProcessId, std::wstring* result)
|
||||
{
|
||||
HRESULT hr = GetProcessId_8(dwProcessId, result);
|
||||
if (!SUCCEEDED(hr))
|
||||
{
|
||||
hr = GetProcessId_7(dwProcessId, result);
|
||||
}
|
||||
return SUCCEEDED(hr);
|
||||
}
|
||||
|
||||
std::wstring GetProcCommandLine(DWORD pid)
|
||||
{
|
||||
std::wstring commandLine;
|
||||
|
||||
// Open a handle to the process
|
||||
const HANDLE process = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid);
|
||||
if (process == NULL)
|
||||
{
|
||||
Logger::error(L"Failed to open the process, error: {}", get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Get the address of the ProcessEnvironmentBlock
|
||||
PROCESS_BASIC_INFORMATION pbi = {};
|
||||
NTSTATUS status = NtQueryInformationProcess(process, ProcessBasicInformation, &pbi, sizeof(pbi), NULL);
|
||||
if (status != STATUS_SUCCESS)
|
||||
{
|
||||
Logger::error(L"Failed to query the process, error: {}", status);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Get the address of the process parameters in the ProcessEnvironmentBlock
|
||||
PEB processEnvironmentBlock = {};
|
||||
if (!ReadProcessMemory(process, pbi.PebBaseAddress, &processEnvironmentBlock, sizeof(processEnvironmentBlock), NULL))
|
||||
{
|
||||
Logger::error(L"Failed to read the process ProcessEnvironmentBlock, error: {}", get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Get the command line arguments from the process parameters
|
||||
RTL_USER_PROCESS_PARAMETERS params = {};
|
||||
if (!ReadProcessMemory(process, processEnvironmentBlock.ProcessParameters, ¶ms, sizeof(params), NULL))
|
||||
{
|
||||
Logger::error(L"Failed to read the process params, error: {}", get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
else
|
||||
{
|
||||
UNICODE_STRING& commandLineArgs = params.CommandLine;
|
||||
std::vector<WCHAR> buffer(commandLineArgs.Length / sizeof(WCHAR));
|
||||
if (!ReadProcessMemory(process, commandLineArgs.Buffer, buffer.data(), commandLineArgs.Length, NULL))
|
||||
{
|
||||
Logger::error(L"Failed to read the process command line, error: {}", get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
else
|
||||
{
|
||||
commandLine.assign(buffer.data(), buffer.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CloseHandle(process);
|
||||
}
|
||||
|
||||
return commandLine;
|
||||
}
|
||||
|
||||
// Finds all PwaHelper.exe processes with the specified parent process ID
|
||||
std::vector<DWORD> FindPwaHelperProcessIds()
|
||||
{
|
||||
std::vector<DWORD> pwaHelperProcessIds;
|
||||
const HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (hSnapshot == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
Logger::info(L"Invalid handle when creating snapshot for the search for PwaHelper processes");
|
||||
return pwaHelperProcessIds;
|
||||
}
|
||||
|
||||
PROCESSENTRY32 pe;
|
||||
pe.dwSize = sizeof(PROCESSENTRY32);
|
||||
|
||||
if (Process32First(hSnapshot, &pe))
|
||||
{
|
||||
do
|
||||
{
|
||||
if (_wcsicmp(pe.szExeFile, L"PwaHelper.exe") == 0)
|
||||
{
|
||||
Logger::info(L"Found a PWA process with id {}", pe.th32ProcessID);
|
||||
pwaHelperProcessIds.push_back(pe.th32ProcessID);
|
||||
}
|
||||
} while (Process32Next(hSnapshot, &pe));
|
||||
}
|
||||
|
||||
CloseHandle(hSnapshot);
|
||||
return pwaHelperProcessIds;
|
||||
}
|
||||
|
||||
void PwaHelper::InitAumidToAppId()
|
||||
{
|
||||
if (pwaAumidToAppId.size() > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const auto pwaHelperProcessIds = FindPwaHelperProcessIds();
|
||||
Logger::info(L"Found {} edge Pwa helper processes", pwaHelperProcessIds.size());
|
||||
for (const auto subProcessID : pwaHelperProcessIds)
|
||||
{
|
||||
std::wstring aumidID;
|
||||
GetProcessId(subProcessID, &aumidID);
|
||||
std::wstring commandLineArg = GetProcCommandLine(subProcessID);
|
||||
auto appIdIndexStart = commandLineArg.find(NonLocalizable::EdgeAppIdIdentifier);
|
||||
if (appIdIndexStart != std::wstring::npos)
|
||||
{
|
||||
commandLineArg = commandLineArg.substr(appIdIndexStart + NonLocalizable::EdgeAppIdIdentifier.size());
|
||||
auto appIdIndexEnd = commandLineArg.find(L" ");
|
||||
if (appIdIndexEnd != std::wstring::npos)
|
||||
{
|
||||
commandLineArg = commandLineArg.substr(0, appIdIndexEnd);
|
||||
}
|
||||
}
|
||||
std::wstring appId{ commandLineArg };
|
||||
pwaAumidToAppId.insert(std::map<std::wstring, std::wstring>::value_type(aumidID, appId));
|
||||
Logger::info(L"Found an edge Pwa helper process with AumidID {} and PwaAppId {}", aumidID, appId);
|
||||
|
||||
PWSTR path = NULL;
|
||||
HRESULT hres = SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, NULL, &path);
|
||||
if (SUCCEEDED(hres))
|
||||
{
|
||||
std::filesystem::path fsPath(path);
|
||||
fsPath /= NonLocalizable::EdgeBase;
|
||||
for (const auto& directory : std::filesystem::directory_iterator(fsPath))
|
||||
{
|
||||
if (directory.is_directory())
|
||||
{
|
||||
const std::filesystem::path directoryName = directory.path().filename();
|
||||
if (directoryName.wstring().find(NonLocalizable::EdgeDirPrefix) == 0)
|
||||
{
|
||||
const std::wstring appIdDir = directoryName.wstring().substr(NonLocalizable::EdgeDirPrefix.size());
|
||||
if (appIdDir == appId)
|
||||
{
|
||||
for (const auto& filename : std::filesystem::directory_iterator(directory))
|
||||
{
|
||||
if (!filename.is_directory())
|
||||
{
|
||||
const std::filesystem::path filenameString = filename.path().filename();
|
||||
if (filenameString.extension().wstring() == L".ico")
|
||||
{
|
||||
pwaAppIdsToAppNames.insert(std::map<std::wstring, std::wstring>::value_type(appId, filenameString.stem().wstring()));
|
||||
Logger::info(L"Storing an edge Pwa app name {} for PwaAppId {}", filenameString.stem().wstring(), appId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CoTaskMemFree(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BOOL PwaHelper::GetPwaAppId(std::wstring windowAumid, std::wstring* result)
|
||||
{
|
||||
const auto pwaIndex = pwaAumidToAppId.find(windowAumid);
|
||||
if (pwaIndex != pwaAumidToAppId.end())
|
||||
{
|
||||
*result = pwaIndex->second;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
BOOL PwaHelper::SearchPwaName(std::wstring pwaAppId, std::wstring windowAumid, std::wstring* pwaName)
|
||||
{
|
||||
const auto index = pwaAppIdsToAppNames.find(pwaAppId);
|
||||
if (index != pwaAppIdsToAppNames.end())
|
||||
{
|
||||
*pwaName = index->second;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::wstring nameFromAumid{ windowAumid };
|
||||
const std::size_t delimiterPos = nameFromAumid.find(L"-");
|
||||
if (delimiterPos != std::string::npos)
|
||||
{
|
||||
nameFromAumid = nameFromAumid.substr(0, delimiterPos);
|
||||
}
|
||||
|
||||
*pwaName = nameFromAumid;
|
||||
return false;
|
||||
}
|
||||
|
||||
void PwaHelper::InitChromeAppIds()
|
||||
{
|
||||
if (chromeAppIds.size() > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PWSTR path = NULL;
|
||||
HRESULT hres = SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, NULL, &path);
|
||||
if (SUCCEEDED(hres))
|
||||
{
|
||||
std::filesystem::path fsPath(path);
|
||||
fsPath /= NonLocalizable::ChromeBase;
|
||||
for (const auto& directory : std::filesystem::directory_iterator(fsPath))
|
||||
{
|
||||
if (directory.is_directory())
|
||||
{
|
||||
const std::filesystem::path directoryName = directory.path().filename();
|
||||
if (directoryName.wstring().find(NonLocalizable::ChromeDirPrefix) == 0)
|
||||
{
|
||||
const std::wstring appId = directoryName.wstring().substr(NonLocalizable::ChromeDirPrefix.size());
|
||||
chromeAppIds.push_back(appId);
|
||||
for (const auto& filename : std::filesystem::directory_iterator(directory))
|
||||
{
|
||||
if (!filename.is_directory())
|
||||
{
|
||||
const std::filesystem::path filenameString = filename.path().filename();
|
||||
if (filenameString.extension().wstring() == L".ico")
|
||||
{
|
||||
pwaAppIdsToAppNames.insert(std::map<std::wstring, std::wstring>::value_type(appId, filenameString.stem().wstring()));
|
||||
Logger::info(L"Found an installed chrome Pwa app {} with PwaAppId {}", filenameString.stem().wstring(), appId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CoTaskMemFree(path);
|
||||
}
|
||||
}
|
||||
|
||||
BOOL PwaHelper::SearchPwaAppId(std::wstring windowAumid, std::wstring* pwaAppId)
|
||||
{
|
||||
const auto appIdIndexStart = windowAumid.find(NonLocalizable::ChromeAppIdIdentifier);
|
||||
if (appIdIndexStart != std::wstring::npos)
|
||||
{
|
||||
windowAumid = windowAumid.substr(appIdIndexStart + NonLocalizable::ChromeAppIdIdentifier.size());
|
||||
const auto appIdIndexEnd = windowAumid.find(L" ");
|
||||
if (appIdIndexEnd != std::wstring::npos)
|
||||
{
|
||||
windowAumid = windowAumid.substr(0, appIdIndexEnd);
|
||||
}
|
||||
|
||||
const std::wstring windowAumidBegin = windowAumid.substr(0, 10);
|
||||
for (const auto chromeAppId : chromeAppIds)
|
||||
{
|
||||
if (chromeAppId.find(windowAumidBegin) == 0)
|
||||
{
|
||||
*pwaAppId = chromeAppId;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*pwaAppId = L"";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
20
src/modules/Workspaces/WorkspacesSnapshotTool/PwaHelper.h
Normal file
20
src/modules/Workspaces/WorkspacesSnapshotTool/PwaHelper.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
namespace SnapshotUtils
|
||||
{
|
||||
class PwaHelper
|
||||
{
|
||||
public:
|
||||
void InitAumidToAppId();
|
||||
BOOL GetAppId(HWND hWnd, std::wstring* result);
|
||||
BOOL GetPwaAppId(std::wstring windowAumid, std::wstring* result);
|
||||
BOOL SearchPwaName(std::wstring pwaAppId, std::wstring windowAumid, std::wstring* pwaName);
|
||||
void InitChromeAppIds();
|
||||
BOOL SearchPwaAppId(std::wstring windowAumid, std::wstring* pwaAppId);
|
||||
|
||||
private:
|
||||
std::map<std::wstring, std::wstring> pwaAumidToAppId;
|
||||
std::vector<std::wstring> chromeAppIds;
|
||||
std::map<std::wstring, std::wstring> pwaAppIdsToAppNames;
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
#include "pch.h"
|
||||
#include "SnapshotUtils.h"
|
||||
|
||||
#include <comdef.h>
|
||||
#include <Wbemidl.h>
|
||||
|
||||
#include <common/utils/elevation.h>
|
||||
#include <common/utils/process_path.h>
|
||||
#include <common/notifications/NotificationUtil.h>
|
||||
@@ -12,142 +9,17 @@
|
||||
#include <workspaces-common/WindowFilter.h>
|
||||
|
||||
#include <WorkspacesLib/AppUtils.h>
|
||||
#include <PwaHelper.h>
|
||||
|
||||
#pragma comment(lib, "ntdll.lib")
|
||||
|
||||
namespace SnapshotUtils
|
||||
{
|
||||
namespace NonLocalizable
|
||||
{
|
||||
const std::wstring ApplicationFrameHost = L"ApplicationFrameHost.exe";
|
||||
}
|
||||
|
||||
class WbemHelper
|
||||
{
|
||||
public:
|
||||
WbemHelper() = default;
|
||||
~WbemHelper()
|
||||
{
|
||||
if (m_services)
|
||||
{
|
||||
m_services->Release();
|
||||
}
|
||||
|
||||
if (m_locator)
|
||||
{
|
||||
m_locator->Release();
|
||||
}
|
||||
}
|
||||
|
||||
bool Initialize()
|
||||
{
|
||||
// Obtain the initial locator to WMI.
|
||||
HRESULT hres = CoCreateInstance(CLSID_WbemLocator, 0, CLSCTX_INPROC_SERVER, IID_IWbemLocator, reinterpret_cast<LPVOID*>(&m_locator));
|
||||
if (FAILED(hres))
|
||||
{
|
||||
Logger::error(L"Failed to create IWbemLocator object. Error: {}", get_last_error_or_default(hres));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Connect to WMI through the IWbemLocator::ConnectServer method.
|
||||
hres = m_locator->ConnectServer(_bstr_t(L"ROOT\\CIMV2"), NULL, NULL, 0, NULL, 0, 0, &m_services);
|
||||
if (FAILED(hres))
|
||||
{
|
||||
Logger::error(L"Could not connect to WMI. Error: {}", get_last_error_or_default(hres));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set security levels on the proxy.
|
||||
hres = CoSetProxyBlanket(m_services, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, NULL, RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE);
|
||||
if (FAILED(hres))
|
||||
{
|
||||
Logger::error(L"Could not set proxy blanket. Error: {}", get_last_error_or_default(hres));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::wstring GetCommandLineArgs(DWORD processID) const
|
||||
{
|
||||
static std::wstring property = L"CommandLine";
|
||||
std::wstring query = L"SELECT " + property + L" FROM Win32_Process WHERE ProcessId = " + std::to_wstring(processID);
|
||||
return Query(query, property);
|
||||
}
|
||||
|
||||
std::wstring GetExecutablePath(DWORD processID) const
|
||||
{
|
||||
static std::wstring property = L"ExecutablePath";
|
||||
std::wstring query = L"SELECT " + property + L" FROM Win32_Process WHERE ProcessId = " + std::to_wstring(processID);
|
||||
return Query(query, property);
|
||||
}
|
||||
|
||||
private:
|
||||
std::wstring Query(const std::wstring& query, const std::wstring& propertyName) const
|
||||
{
|
||||
if (!m_locator || !m_services)
|
||||
{
|
||||
return L"";
|
||||
}
|
||||
|
||||
IEnumWbemClassObject* pEnumerator = NULL;
|
||||
|
||||
HRESULT hres = m_services->ExecQuery(bstr_t("WQL"), bstr_t(query.c_str()), WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, NULL, &pEnumerator);
|
||||
if (FAILED(hres))
|
||||
{
|
||||
Logger::error(L"Query for process failed. Error: {}", get_last_error_or_default(hres));
|
||||
return L"";
|
||||
}
|
||||
|
||||
IWbemClassObject* pClassObject = NULL;
|
||||
ULONG uReturn = 0;
|
||||
std::wstring result = L"";
|
||||
while (pEnumerator)
|
||||
{
|
||||
HRESULT hr = pEnumerator->Next(WBEM_INFINITE, 1, &pClassObject, &uReturn);
|
||||
if (uReturn == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
VARIANT vtProp;
|
||||
hr = pClassObject->Get(propertyName.c_str(), 0, &vtProp, 0, 0);
|
||||
if (SUCCEEDED(hr) && vtProp.vt == VT_BSTR)
|
||||
{
|
||||
result = vtProp.bstrVal;
|
||||
}
|
||||
VariantClear(&vtProp);
|
||||
|
||||
pClassObject->Release();
|
||||
}
|
||||
|
||||
pEnumerator->Release();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
IWbemLocator* m_locator = NULL;
|
||||
IWbemServices* m_services = NULL;
|
||||
};
|
||||
|
||||
std::wstring GetCommandLineArgs(DWORD processID, const WbemHelper& wbemHelper)
|
||||
{
|
||||
std::wstring executablePath = wbemHelper.GetExecutablePath(processID);
|
||||
std::wstring commandLineArgs = wbemHelper.GetCommandLineArgs(processID);
|
||||
|
||||
if (!commandLineArgs.empty())
|
||||
{
|
||||
auto pos = commandLineArgs.find(executablePath);
|
||||
if (pos != std::wstring::npos)
|
||||
{
|
||||
commandLineArgs = commandLineArgs.substr(pos + executablePath.size());
|
||||
auto spacePos = commandLineArgs.find_first_of(' ');
|
||||
if (spacePos != std::wstring::npos)
|
||||
{
|
||||
commandLineArgs = commandLineArgs.substr(spacePos + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return commandLineArgs;
|
||||
const std::wstring EdgeFilename = L"msedge.exe";
|
||||
const std::wstring ChromeFilename = L"chrome.exe";
|
||||
}
|
||||
|
||||
bool IsProcessElevated(DWORD processID)
|
||||
@@ -168,16 +40,23 @@ namespace SnapshotUtils
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsEdge(Utils::Apps::AppData appData)
|
||||
{
|
||||
return appData.installPath.ends_with(NonLocalizable::EdgeFilename);
|
||||
}
|
||||
|
||||
bool IsChrome(Utils::Apps::AppData appData)
|
||||
{
|
||||
return appData.installPath.ends_with(NonLocalizable::ChromeFilename);
|
||||
}
|
||||
|
||||
std::vector<WorkspacesData::WorkspacesProject::Application> GetApps(const std::function<unsigned int(HWND)> getMonitorNumberFromWindowHandle, const std::function<WorkspacesData::WorkspacesProject::Monitor::MonitorRect(unsigned int)> getMonitorRect)
|
||||
{
|
||||
PwaHelper pwaHelper{};
|
||||
std::vector<WorkspacesData::WorkspacesProject::Application> apps{};
|
||||
|
||||
auto installedApps = Utils::Apps::GetAppsList();
|
||||
auto windows = WindowEnumerator::Enumerate(WindowFilter::Filter);
|
||||
|
||||
// for command line args detection
|
||||
// WbemHelper wbemHelper;
|
||||
// wbemHelper.Initialize();
|
||||
|
||||
for (const auto window : windows)
|
||||
{
|
||||
@@ -232,7 +111,7 @@ namespace SnapshotUtils
|
||||
if (pid != otherPid && title == WindowUtils::GetWindowTitle(otherWindow))
|
||||
{
|
||||
processPath = get_process_path(otherPid);
|
||||
break;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -249,6 +128,48 @@ namespace SnapshotUtils
|
||||
continue;
|
||||
}
|
||||
|
||||
std::wstring pwaAppId = L"";
|
||||
std::wstring finalName = data.value().name;
|
||||
std::wstring pwaName = L"";
|
||||
if (IsEdge(data.value()))
|
||||
{
|
||||
pwaHelper.InitAumidToAppId();
|
||||
|
||||
std::wstring windowAumid;
|
||||
pwaHelper.GetAppId(window, &windowAumid);
|
||||
Logger::info(L"Found an edge window with aumid {}", windowAumid);
|
||||
|
||||
if (pwaHelper.GetPwaAppId(windowAumid, &pwaAppId))
|
||||
{
|
||||
Logger::info(L"The found edge window is a PWA app with appId {}", pwaAppId);
|
||||
if (pwaHelper.SearchPwaName(pwaAppId, windowAumid ,& pwaName))
|
||||
{
|
||||
Logger::info(L"The found edge window is a PWA app with name {}", finalName);
|
||||
}
|
||||
finalName = pwaName + L" (" + finalName + L")";
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::info(L"The found edge window does not contain a PWA app", pwaAppId);
|
||||
}
|
||||
}
|
||||
else if (IsChrome(data.value()))
|
||||
{
|
||||
pwaHelper.InitChromeAppIds();
|
||||
|
||||
std::wstring windowAumid;
|
||||
pwaHelper.GetAppId(window, &windowAumid);
|
||||
Logger::info(L"Found a chrome window with aumid {}", windowAumid);
|
||||
|
||||
if (pwaHelper.SearchPwaAppId(windowAumid, &pwaAppId))
|
||||
{
|
||||
if (pwaHelper.SearchPwaName(pwaAppId, windowAumid, &pwaName))
|
||||
{
|
||||
finalName = pwaName + L" (" + finalName + L")";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool isMinimized = WindowUtils::IsMinimized(window);
|
||||
unsigned int monitorNumber = getMonitorNumberFromWindowHandle(window);
|
||||
|
||||
@@ -263,12 +184,13 @@ namespace SnapshotUtils
|
||||
}
|
||||
|
||||
WorkspacesData::WorkspacesProject::Application app{
|
||||
.name = data.value().name,
|
||||
.name = finalName,
|
||||
.title = title,
|
||||
.path = data.value().installPath,
|
||||
.packageFullName = data.value().packageFullName,
|
||||
.appUserModelId = data.value().appUserModelId,
|
||||
.commandLineArgs = L"", // GetCommandLineArgs(pid, wbemHelper),
|
||||
.pwaAppId = pwaAppId,
|
||||
.commandLineArgs = L"",
|
||||
.isElevated = IsProcessElevated(pid),
|
||||
.canLaunchElevated = data.value().canLaunchElevated,
|
||||
.isMinimized = isMinimized,
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>shcore.lib;Shell32.lib;propsys.lib;DbgHelp.lib;wbemuuid.lib</AdditionalDependencies>
|
||||
<AdditionalDependencies>shcore.lib;Shell32.lib;propsys.lib;DbgHelp.lib</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
@@ -130,10 +130,12 @@
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="PwaHelper.cpp" />
|
||||
<ClCompile Include="SnapshotUtils.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="PwaHelper.h" />
|
||||
<ClInclude Include="resource.base.h" />
|
||||
<ClInclude Include="SnapshotUtils.h" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
<ClInclude Include="resource.base.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="PwaHelper.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="pch.cpp">
|
||||
@@ -35,6 +38,9 @@
|
||||
<ClCompile Include="SnapshotUtils.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="PwaHelper.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
@@ -21,6 +22,7 @@ public sealed partial class AllAppsPage : ListPage
|
||||
this.ShowDetails = true;
|
||||
this.Loading = true;
|
||||
this.PlaceholderText = "Search installed apps...";
|
||||
this.AccentColor = Color.FromArgb(255, 255, 102, 0);
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems()
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// 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.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class ActionBarViewModel : ObservableObject
|
||||
{
|
||||
public ListItemViewModel? SelectedItem
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
SetSelectedItem(value);
|
||||
}
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string PrimaryActionName { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string SecondaryActionName { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ShouldShowContextMenu { get; set; } = false;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<CommandContextItemViewModel> ContextActions { get; set; } = [];
|
||||
|
||||
public ActionBarViewModel()
|
||||
{
|
||||
}
|
||||
|
||||
private void SetSelectedItem(ListItemViewModel? value)
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
PrimaryActionName = value.Name;
|
||||
SecondaryActionName = value.SecondaryCommandName;
|
||||
|
||||
if (value.MoreCommands.Count > 0)
|
||||
{
|
||||
ShouldShowContextMenu = true;
|
||||
ContextActions = [.. value.AllCommands];
|
||||
}
|
||||
else
|
||||
{
|
||||
ShouldShowContextMenu = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
PrimaryActionName = string.Empty;
|
||||
SecondaryActionName = string.Empty;
|
||||
ShouldShowContextMenu = false;
|
||||
}
|
||||
}
|
||||
|
||||
// InvokeItemCommand is what this will be in Xaml due to source generator
|
||||
[RelayCommand]
|
||||
private void InvokeItem(CommandContextItemViewModel item) => WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.Command));
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// 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.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class CommandContextItemViewModel(ICommandContextItem contextItem) : CommandItemViewModel(new(contextItem))
|
||||
{
|
||||
private readonly ExtensionObject<ICommandContextItem> _contextItemModel = new(contextItem);
|
||||
|
||||
public bool IsCritical { get; private set; }
|
||||
|
||||
public KeyChord? RequestedShortcut { get; private set; }
|
||||
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
base.InitializeProperties();
|
||||
|
||||
var contextItem = _contextItemModel.Unsafe;
|
||||
if (contextItem == null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
|
||||
IsCritical = contextItem.IsCritical;
|
||||
if (contextItem.RequestedShortcut != null)
|
||||
{
|
||||
RequestedShortcut = new(
|
||||
contextItem.RequestedShortcut.Modifiers,
|
||||
contextItem.RequestedShortcut.Vkey,
|
||||
contextItem.RequestedShortcut.ScanCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// 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 CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class CommandItemViewModel : ObservableObject
|
||||
{
|
||||
private readonly ExtensionObject<ICommandItem> _commandItemModel = new(null);
|
||||
|
||||
// private bool _initialized;
|
||||
public string Name { get; private set; } = string.Empty;
|
||||
|
||||
public string Title { get; private set; } = string.Empty;
|
||||
|
||||
public string Subtitle { get; private set; } = string.Empty;
|
||||
|
||||
public string IconUri { get; private set; } = string.Empty;
|
||||
|
||||
public ExtensionObject<ICommand> Command { get; private set; } = new(null);
|
||||
|
||||
public List<CommandContextItemViewModel> MoreCommands { get; private set; } = [];
|
||||
|
||||
public bool HasMoreCommands => MoreCommands.Count > 0;
|
||||
|
||||
public string SecondaryCommandName => HasMoreCommands ? MoreCommands[0].Name : string.Empty;
|
||||
|
||||
public List<CommandContextItemViewModel> AllCommands
|
||||
{
|
||||
get
|
||||
{
|
||||
var command = _commandItemModel.Unsafe?.Command;
|
||||
var model = new CommandContextItem(command!)
|
||||
{
|
||||
};
|
||||
CommandContextItemViewModel defaultCommand = new(model)
|
||||
{
|
||||
Name = Name,
|
||||
Title = Name,
|
||||
Subtitle = Subtitle,
|
||||
IconUri = IconUri,
|
||||
|
||||
// TODO this probably should just be a CommandContextItemViewModel(CommandItemViewModel) ctor, or a copy ctor or whatever
|
||||
};
|
||||
|
||||
var l = new List<CommandContextItemViewModel> { defaultCommand };
|
||||
l.AddRange(MoreCommands);
|
||||
return l;
|
||||
}
|
||||
}
|
||||
|
||||
public CommandItemViewModel(ExtensionObject<ICommandItem> item)
|
||||
{
|
||||
_commandItemModel = item;
|
||||
}
|
||||
|
||||
//// Called from ListViewModel on background thread started in ListPage.xaml.cs
|
||||
public virtual void InitializeProperties()
|
||||
{
|
||||
var model = _commandItemModel.Unsafe;
|
||||
if (model == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Command = new(model.Command);
|
||||
Name = model.Command?.Name ?? string.Empty;
|
||||
Title = model.Title;
|
||||
Subtitle = model.Subtitle;
|
||||
IconUri = model.Icon.Icon;
|
||||
MoreCommands = model.MoreCommands
|
||||
.Where(contextItem => contextItem is ICommandContextItem)
|
||||
.Select(contextItem => (contextItem as ICommandContextItem)!)
|
||||
.Select(contextItem => new CommandContextItemViewModel(contextItem))
|
||||
.ToList();
|
||||
|
||||
// Here, we're already theoretically in the async context, so we can
|
||||
// use Initialize straight up
|
||||
MoreCommands.ForEach(contextItem =>
|
||||
{
|
||||
contextItem.InitializeProperties();
|
||||
});
|
||||
|
||||
model.PropChanged += Model_PropChanged;
|
||||
|
||||
// _initialized = true;
|
||||
}
|
||||
|
||||
private void Model_PropChanged(object sender, PropChangedEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
FetchProperty(args.PropertyName);
|
||||
OnPropertyChanged(args.PropertyName);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// TODO log? throw?
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void FetchProperty(string propertyName)
|
||||
{
|
||||
var model = this._commandItemModel.Unsafe;
|
||||
if (model == null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
|
||||
switch (propertyName)
|
||||
{
|
||||
case nameof(Name):
|
||||
this.Name = model.Command?.Name ?? string.Empty;
|
||||
break;
|
||||
case nameof(Title):
|
||||
this.Title = model.Title;
|
||||
break;
|
||||
case nameof(Subtitle):
|
||||
this.Subtitle = model.Subtitle;
|
||||
break;
|
||||
|
||||
// TODO! Icon
|
||||
// TODO! MoreCommands array, which needs to also raise HasMoreCommands
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,8 @@ public sealed class CommandProviderWrapper
|
||||
private readonly ICommandProvider _commandProvider;
|
||||
|
||||
private readonly IExtensionWrapper? extensionWrapper;
|
||||
private ICommandItem[] _topLevelItems = [];
|
||||
|
||||
public ICommandItem[] TopLevelItems => _topLevelItems;
|
||||
public ICommandItem[] TopLevelItems { get; private set; } = [];
|
||||
|
||||
public CommandProviderWrapper(ICommandProvider provider)
|
||||
{
|
||||
@@ -29,8 +28,14 @@ public sealed class CommandProviderWrapper
|
||||
public CommandProviderWrapper(IExtensionWrapper extension)
|
||||
{
|
||||
extensionWrapper = extension;
|
||||
if (!extensionWrapper.IsRunning())
|
||||
{
|
||||
throw new ArgumentException("You forgot to start the extension. This is a coding error - make sure to call StartExtensionAsync");
|
||||
}
|
||||
|
||||
var extensionImpl = extension.GetExtensionObject();
|
||||
if (extensionImpl?.GetProvider(ProviderType.Commands) is not ICommandProvider provider)
|
||||
var providerObject = extensionImpl?.GetProvider(ProviderType.Commands);
|
||||
if (providerObject is not ICommandProvider provider)
|
||||
{
|
||||
throw new ArgumentException("extension didn't actually implement ICommandProvider");
|
||||
}
|
||||
@@ -46,14 +51,14 @@ public sealed class CommandProviderWrapper
|
||||
return;
|
||||
}
|
||||
|
||||
var t = new Task<ICommandItem[]>(() => _commandProvider.TopLevelCommands());
|
||||
var t = new Task<ICommandItem[]>(_commandProvider.TopLevelCommands);
|
||||
t.Start();
|
||||
var commands = await t.ConfigureAwait(false);
|
||||
|
||||
// On a BG thread here
|
||||
if (commands != null)
|
||||
{
|
||||
_topLevelItems = commands;
|
||||
TopLevelItems = commands;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// 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.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class ListItemViewModel(IListItem model) : CommandItemViewModel(new(model))
|
||||
{
|
||||
private readonly ExtensionObject<IListItem> _listItemModel = new(model);
|
||||
|
||||
public ITag[] Tags { get; private set; } = [];
|
||||
|
||||
public bool HasTags => Tags.Length > 0;
|
||||
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
base.InitializeProperties();
|
||||
|
||||
// TODO load tags here, details, suggested text, all that
|
||||
var li = _listItemModel.Unsafe;
|
||||
if (li == null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
|
||||
// TODO TagViewModel not ITag
|
||||
Tags = li.Tags ?? [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// 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 CommunityToolkit.Mvvm.Collections;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class ListViewModel : ObservableObject
|
||||
{
|
||||
// Observable from MVVM Toolkit will auto create public properties that use INotifyPropertyChange change
|
||||
// https://learn.microsoft.com/dotnet/communitytoolkit/mvvm/observablegroupedcollections for grouping support
|
||||
[ObservableProperty]
|
||||
public partial ObservableGroupedCollection<string, ListItemViewModel> Items { get; set; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsInitialized { get; private set; }
|
||||
|
||||
private readonly ExtensionObject<IListPage> _model;
|
||||
|
||||
public ListViewModel(IListPage model)
|
||||
{
|
||||
_model = new(model);
|
||||
}
|
||||
|
||||
private void Model_ItemsChanged(object sender, ItemsChangedEventArgs args) => FetchItems();
|
||||
|
||||
//// Run on background thread, from InitializeAsync or Model_ItemsChanged
|
||||
private void FetchItems()
|
||||
{
|
||||
ObservableGroup<string, ListItemViewModel> group = new(string.Empty);
|
||||
|
||||
// TEMPORARY: just plop all the items into a single group
|
||||
// see 9806fe5d8 for the last commit that had this with sections
|
||||
// TODO unsafe
|
||||
var newItems = _model.Unsafe!.GetItems();
|
||||
|
||||
Items.Clear();
|
||||
|
||||
foreach (var item in newItems)
|
||||
{
|
||||
ListItemViewModel viewModel = new(item);
|
||||
viewModel.InitializeProperties();
|
||||
group.Add(viewModel);
|
||||
}
|
||||
|
||||
// Am I really allowed to modify that observable collection on a BG
|
||||
// thread and have it just work in the UI??
|
||||
Items.AddGroup(group);
|
||||
}
|
||||
|
||||
//// Run on background thread from ListPage.xaml.cs
|
||||
[RelayCommand]
|
||||
private Task<bool> InitializeAsync()
|
||||
{
|
||||
// TODO: We may want a SemaphoreSlim lock here.
|
||||
|
||||
// TODO: We may want to investigate using some sort of AsyncEnumerable or populating these as they come in to the UI layer
|
||||
// Though we have to think about threading here and circling back to the UI thread with a TaskScheduler.
|
||||
FetchItems();
|
||||
|
||||
_model.Unsafe!.ItemsChanged += Model_ItemsChanged;
|
||||
|
||||
IsInitialized = true;
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
// InvokeItemCommand is what this will be in Xaml due to source generator
|
||||
[RelayCommand]
|
||||
private void InvokeItem(ListItemViewModel item) => WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.Command));
|
||||
|
||||
[RelayCommand]
|
||||
private void UpdateSelectedItem(ListItemViewModel item) => WeakReferenceMessenger.Default.Send<UpdateActionBarMessage>(new(item));
|
||||
}
|
||||
@@ -2,9 +2,12 @@
|
||||
// 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.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Pages;
|
||||
|
||||
@@ -14,16 +17,26 @@ namespace Microsoft.CmdPal.UI.Pages;
|
||||
/// </summary>
|
||||
public partial class MainListPage : DynamicListPage
|
||||
{
|
||||
private readonly IListItem[] _items;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
// TODO: Thinking we may want a separate MainViewModel from the ShellViewModel and/or a CommandService/Provider
|
||||
// which holds the TopLevelCommands and anything that needs to access those functions...
|
||||
public MainListPage(ShellViewModel shellViewModel)
|
||||
private readonly ObservableCollection<TopLevelCommandWrapper> _commands;
|
||||
|
||||
public MainListPage(IServiceProvider serviceProvider)
|
||||
{
|
||||
_items = shellViewModel.TopLevelCommands.Select(w => w.Unsafe).Where(li => li != null).ToArray();
|
||||
_serviceProvider = serviceProvider;
|
||||
|
||||
var tlcManager = _serviceProvider.GetService<TopLevelCommandManager>();
|
||||
|
||||
// reference the TLC collection directly... maybe? TODO is this a good idea ot a terrible one?
|
||||
_commands = tlcManager!.TopLevelCommands;
|
||||
_commands.CollectionChanged += Commands_CollectionChanged;
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems() => _items;
|
||||
private void Commands_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) => RaiseItemsChanged(_commands.Count);
|
||||
|
||||
public override IListItem[] GetItems() => _commands
|
||||
.Select(tlc => tlc)
|
||||
.ToArray();
|
||||
|
||||
public override void UpdateSearchText(string oldSearch, string newSearch)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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.UI.ViewModels.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Used to perform a list item's command when the user presses enter in the search box
|
||||
/// </summary>
|
||||
public record ActivateSelectedListItemMessage
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// 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.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Used to do a command - navigate to a page or invoke it
|
||||
/// </summary>
|
||||
public record PerformCommandMessage(ExtensionObject<ICommand> Command)
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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.UI.ViewModels.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Used to update the action bar at the bottom to refelct the commands for a list item
|
||||
/// </summary>
|
||||
public record UpdateActionBarMessage(ListItemViewModel ViewModel)
|
||||
{
|
||||
}
|
||||
@@ -14,6 +14,10 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -2,24 +2,9 @@
|
||||
// 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.Models;
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
|
||||
public class ExtensionObject<T> // where T : IInspectable
|
||||
public class ExtensionObject<T>(T? value) // where T : IInspectable
|
||||
{
|
||||
private readonly T _value;
|
||||
|
||||
public ExtensionObject(T value)
|
||||
{
|
||||
_value = value;
|
||||
}
|
||||
|
||||
// public T? Safe {
|
||||
// get {
|
||||
// try {
|
||||
// if (_value!.Equals(_value)) return _value;
|
||||
// } catch (COMException){ /* log something */ }
|
||||
// return default;
|
||||
// }
|
||||
// }
|
||||
public T Unsafe => _value;
|
||||
public T? Unsafe { get; } = value;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
// 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.CmdPal.Common.Services;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Windows.ApplicationModel;
|
||||
using Windows.ApplicationModel.AppExtensions;
|
||||
using Windows.Foundation.Collections;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
|
||||
public class ExtensionService : IExtensionService, IDisposable
|
||||
{
|
||||
public event EventHandler OnExtensionsChanged = (_, _) => { };
|
||||
|
||||
private static readonly PackageCatalog _catalog = PackageCatalog.OpenForCurrentUser();
|
||||
private static readonly Lock _lock = new();
|
||||
private readonly SemaphoreSlim _getInstalledExtensionsLock = new(1, 1);
|
||||
private readonly SemaphoreSlim _getInstalledWidgetsLock = new(1, 1);
|
||||
|
||||
// private readonly ILocalSettingsService _localSettingsService;
|
||||
private bool _disposedValue;
|
||||
|
||||
private const string CreateInstanceProperty = "CreateInstance";
|
||||
private const string ClassIdProperty = "@ClassId";
|
||||
|
||||
private static readonly List<IExtensionWrapper> _installedExtensions = [];
|
||||
private static readonly List<IExtensionWrapper> _enabledExtensions = [];
|
||||
|
||||
public ExtensionService()
|
||||
{
|
||||
_catalog.PackageInstalling += Catalog_PackageInstalling;
|
||||
_catalog.PackageUninstalling += Catalog_PackageUninstalling;
|
||||
_catalog.PackageUpdating += Catalog_PackageUpdating;
|
||||
|
||||
//// These two were an investigation into getting updates when a package
|
||||
//// gets redeployed from VS. Neither get raised (nor do the above)
|
||||
//// _catalog.PackageStatusChanged += Catalog_PackageStatusChanged;
|
||||
//// _catalog.PackageStaging += Catalog_PackageStaging;
|
||||
// _localSettingsService = settingsService;
|
||||
}
|
||||
|
||||
private void Catalog_PackageInstalling(PackageCatalog sender, PackageInstallingEventArgs args)
|
||||
{
|
||||
if (args.IsComplete)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var isCmdPalExtension = Task.Run(() =>
|
||||
{
|
||||
return IsValidCmdPalExtension(args.Package);
|
||||
}).Result;
|
||||
|
||||
if (isCmdPalExtension)
|
||||
{
|
||||
OnPackageChange(args.Package);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Catalog_PackageUninstalling(PackageCatalog sender, PackageUninstallingEventArgs args)
|
||||
{
|
||||
if (args.IsComplete)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var extension in _installedExtensions)
|
||||
{
|
||||
if (extension.PackageFullName == args.Package.Id.FullName)
|
||||
{
|
||||
OnPackageChange(args.Package);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Catalog_PackageUpdating(PackageCatalog sender, PackageUpdatingEventArgs args)
|
||||
{
|
||||
if (args.IsComplete)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var isCmdPalExtension = Task.Run(() =>
|
||||
{
|
||||
return IsValidCmdPalExtension(args.TargetPackage);
|
||||
}).Result;
|
||||
|
||||
if (isCmdPalExtension)
|
||||
{
|
||||
OnPackageChange(args.TargetPackage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPackageChange(Package package)
|
||||
{
|
||||
_installedExtensions.Clear();
|
||||
_enabledExtensions.Clear();
|
||||
OnExtensionsChanged.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private static async Task<bool> IsValidCmdPalExtension(Package package)
|
||||
{
|
||||
var extensions = await AppExtensionCatalog.Open("com.microsoft.windows.commandpalette").FindAllAsync();
|
||||
foreach (var extension in extensions)
|
||||
{
|
||||
if (package.Id?.FullName == extension.Package?.Id?.FullName)
|
||||
{
|
||||
var (cmdPalProvider, classId) = await GetCmdPalExtensionPropertiesAsync(extension);
|
||||
|
||||
return cmdPalProvider != null && classId.Count != 0;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static async Task<(IPropertySet? CmdPalProvider, List<string> ClassIds)> GetCmdPalExtensionPropertiesAsync(AppExtension extension)
|
||||
{
|
||||
var classIds = new List<string>();
|
||||
var properties = await extension.GetExtensionPropertiesAsync();
|
||||
|
||||
if (properties is null)
|
||||
{
|
||||
return (null, classIds);
|
||||
}
|
||||
|
||||
var cmdPalProvider = GetSubPropertySet(properties, "CmdPalProvider");
|
||||
if (cmdPalProvider is null)
|
||||
{
|
||||
return (null, classIds);
|
||||
}
|
||||
|
||||
var activation = GetSubPropertySet(cmdPalProvider, "Activation");
|
||||
if (activation is null)
|
||||
{
|
||||
return (cmdPalProvider, classIds);
|
||||
}
|
||||
|
||||
// Handle case where extension creates multiple instances.
|
||||
classIds.AddRange(GetCreateInstanceList(activation));
|
||||
|
||||
return (cmdPalProvider, classIds);
|
||||
}
|
||||
|
||||
private static async Task<IEnumerable<AppExtension>> GetInstalledAppExtensionsAsync() => await AppExtensionCatalog.Open("com.microsoft.windows.commandpalette").FindAllAsync();
|
||||
|
||||
public async Task<IEnumerable<IExtensionWrapper>> GetInstalledExtensionsAsync(bool includeDisabledExtensions = false)
|
||||
{
|
||||
await _getInstalledExtensionsLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (_installedExtensions.Count == 0)
|
||||
{
|
||||
var extensions = await GetInstalledAppExtensionsAsync();
|
||||
foreach (var extension in extensions)
|
||||
{
|
||||
var (cmdPalProvider, classIds) = await GetCmdPalExtensionPropertiesAsync(extension);
|
||||
|
||||
if (cmdPalProvider == null || classIds.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var classId in classIds)
|
||||
{
|
||||
var extensionWrapper = new ExtensionWrapper(extension, classId);
|
||||
|
||||
var supportedInterfaces = GetSubPropertySet(cmdPalProvider, "SupportedInterfaces");
|
||||
if (supportedInterfaces is not null)
|
||||
{
|
||||
foreach (var supportedInterface in supportedInterfaces)
|
||||
{
|
||||
ProviderType pt;
|
||||
if (Enum.TryParse(supportedInterface.Key, out pt))
|
||||
{
|
||||
extensionWrapper.AddProviderType(pt);
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: throw warning or fire notification that extension declared unsupported extension interface
|
||||
// https://github.com/microsoft/DevHome/issues/617
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// var localSettingsService = Application.Current.GetService<ILocalSettingsService>();
|
||||
var extensionUniqueId = extension.AppInfo.AppUserModelId + "!" + extension.Id;
|
||||
var isExtensionDisabled = false; // await localSettingsService.ReadSettingAsync<bool>(extensionUniqueId + "-ExtensionDisabled");
|
||||
|
||||
_installedExtensions.Add(extensionWrapper);
|
||||
if (!isExtensionDisabled)
|
||||
{
|
||||
_enabledExtensions.Add(extensionWrapper);
|
||||
}
|
||||
|
||||
// TelemetryFactory.Get<ITelemetry>().Log(
|
||||
// "Extension_ReportInstalled",
|
||||
// LogLevel.Critical,
|
||||
// new ReportInstalledExtensionEvent(extensionUniqueId, isEnabled: !isExtensionDisabled));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return includeDisabledExtensions ? _installedExtensions : _enabledExtensions;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_getInstalledExtensionsLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public IExtensionWrapper? GetInstalledExtension(string extensionUniqueId)
|
||||
{
|
||||
var extension = _installedExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal));
|
||||
return extension.FirstOrDefault();
|
||||
}
|
||||
|
||||
public async Task SignalStopExtensionsAsync()
|
||||
{
|
||||
var installedExtensions = await GetInstalledExtensionsAsync();
|
||||
foreach (var installedExtension in installedExtensions)
|
||||
{
|
||||
if (installedExtension.IsRunning())
|
||||
{
|
||||
installedExtension.SignalDispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<IExtensionWrapper>> GetInstalledExtensionsAsync(ProviderType providerType, bool includeDisabledExtensions = false)
|
||||
{
|
||||
var installedExtensions = await GetInstalledExtensionsAsync(includeDisabledExtensions);
|
||||
|
||||
List<IExtensionWrapper> filteredExtensions = [];
|
||||
foreach (var installedExtension in installedExtensions)
|
||||
{
|
||||
if (installedExtension.HasProviderType(providerType))
|
||||
{
|
||||
filteredExtensions.Add(installedExtension);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredExtensions;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_getInstalledExtensionsLock.Dispose();
|
||||
_getInstalledWidgetsLock.Dispose();
|
||||
}
|
||||
|
||||
_disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static IPropertySet? GetSubPropertySet(IPropertySet propSet, string name) => propSet.TryGetValue(name, out var value) ? value as IPropertySet : null;
|
||||
|
||||
private static object[]? GetSubPropertySetArray(IPropertySet propSet, string name) => propSet.TryGetValue(name, out var value) ? value as object[] : null;
|
||||
|
||||
/// <summary>
|
||||
/// There are cases where the extension creates multiple COM instances.
|
||||
/// </summary>
|
||||
/// <param name="activationPropSet">Activation property set object</param>
|
||||
/// <returns>List of ClassId strings associated with the activation property</returns>
|
||||
private static List<string> GetCreateInstanceList(IPropertySet activationPropSet)
|
||||
{
|
||||
var propSetList = new List<string>();
|
||||
var singlePropertySet = GetSubPropertySet(activationPropSet, CreateInstanceProperty);
|
||||
if (singlePropertySet != null)
|
||||
{
|
||||
var classId = GetProperty(singlePropertySet, ClassIdProperty);
|
||||
|
||||
// If the instance has a classId as a single string, then it's only supporting a single instance.
|
||||
if (classId != null)
|
||||
{
|
||||
propSetList.Add(classId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var propertySetArray = GetSubPropertySetArray(activationPropSet, CreateInstanceProperty);
|
||||
if (propertySetArray != null)
|
||||
{
|
||||
foreach (var prop in propertySetArray)
|
||||
{
|
||||
if (prop is not IPropertySet propertySet)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var classId = GetProperty(propertySet, ClassIdProperty);
|
||||
if (classId != null)
|
||||
{
|
||||
propSetList.Add(classId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return propSetList;
|
||||
}
|
||||
|
||||
private static string? GetProperty(IPropertySet propSet, string name) => propSet[name] as string;
|
||||
|
||||
public void EnableExtension(string extensionUniqueId)
|
||||
{
|
||||
var extension = _installedExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal));
|
||||
_enabledExtensions.Add(extension.First());
|
||||
}
|
||||
|
||||
public void DisableExtension(string extensionUniqueId)
|
||||
{
|
||||
var extension = _enabledExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal));
|
||||
_enabledExtensions.Remove(extension.First());
|
||||
}
|
||||
|
||||
/*
|
||||
///// <inheritdoc cref="IExtensionService.DisableExtensionIfWindowsFeatureNotAvailable(IExtensionWrapper)"/>
|
||||
//public async Task<bool> DisableExtensionIfWindowsFeatureNotAvailable(IExtensionWrapper extension)
|
||||
//{
|
||||
// // Only attempt to disable feature if its available.
|
||||
// if (IsWindowsOptionalFeatureAvailableForExtension(extension.ExtensionClassId))
|
||||
// {
|
||||
// return false;
|
||||
// }
|
||||
// _log.Warning($"Disabling extension: '{extension.ExtensionDisplayName}' because its feature is absent or unknown");
|
||||
// // Remove extension from list of enabled extensions to prevent Dev Home from re-querying for this extension
|
||||
// // for the rest of its process lifetime.
|
||||
// DisableExtension(extension.ExtensionUniqueId);
|
||||
// // Update the local settings so the next time the user launches Dev Home the extension will be disabled.
|
||||
// await _localSettingsService.SaveSettingAsync(extension.ExtensionUniqueId + "-ExtensionDisabled", true);
|
||||
// return true;
|
||||
//} */
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
// 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.InteropServices;
|
||||
using Microsoft.CmdPal.Common.Services;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Windows.ApplicationModel;
|
||||
using Windows.ApplicationModel.AppExtensions;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.System.Com;
|
||||
using WinRT;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
|
||||
public class ExtensionWrapper : IExtensionWrapper
|
||||
{
|
||||
private const int HResultRpcServerNotRunning = -2147023174;
|
||||
|
||||
private readonly Lock _lock = new();
|
||||
private readonly List<ProviderType> _providerTypes = [];
|
||||
|
||||
private readonly Dictionary<Type, ProviderType> _providerTypeMap = new()
|
||||
{
|
||||
[typeof(ICommandProvider)] = ProviderType.Commands,
|
||||
};
|
||||
|
||||
private IExtension? _extensionObject;
|
||||
|
||||
public ExtensionWrapper(AppExtension appExtension, string classId)
|
||||
{
|
||||
PackageDisplayName = appExtension.Package.DisplayName;
|
||||
ExtensionDisplayName = appExtension.DisplayName;
|
||||
PackageFullName = appExtension.Package.Id.FullName;
|
||||
PackageFamilyName = appExtension.Package.Id.FamilyName;
|
||||
ExtensionClassId = classId ?? throw new ArgumentNullException(nameof(classId));
|
||||
Publisher = appExtension.Package.PublisherDisplayName;
|
||||
InstalledDate = appExtension.Package.InstalledDate;
|
||||
Version = appExtension.Package.Id.Version;
|
||||
ExtensionUniqueId = appExtension.AppInfo.AppUserModelId + "!" + appExtension.Id;
|
||||
}
|
||||
|
||||
public string PackageDisplayName { get; }
|
||||
|
||||
public string ExtensionDisplayName { get; }
|
||||
|
||||
public string PackageFullName { get; }
|
||||
|
||||
public string PackageFamilyName { get; }
|
||||
|
||||
public string ExtensionClassId { get; }
|
||||
|
||||
public string Publisher { get; }
|
||||
|
||||
public DateTimeOffset InstalledDate { get; }
|
||||
|
||||
public PackageVersion Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique id for this Dev Home extension. The unique id is a concatenation of:
|
||||
/// <list type="number">
|
||||
/// <item>The AppUserModelId (AUMID) of the extension's application. The AUMID is the concatenation of the package
|
||||
/// family name and the application id and uniquely identifies the application containing the extension within
|
||||
/// the package.</item>
|
||||
/// <item>The Extension Id. This is the unique identifier of the extension within the application.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public string ExtensionUniqueId { get; }
|
||||
|
||||
public bool IsRunning()
|
||||
{
|
||||
if (_extensionObject is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_extensionObject.As<IInspectable>().GetRuntimeClassName();
|
||||
}
|
||||
catch (COMException e)
|
||||
{
|
||||
if (e.ErrorCode == HResultRpcServerNotRunning)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task StartExtensionAsync()
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!IsRunning())
|
||||
{
|
||||
var extensionPtr = nint.Zero;
|
||||
try
|
||||
{
|
||||
var hr = PInvoke.CoCreateInstance(Guid.Parse(ExtensionClassId), null, CLSCTX.CLSCTX_LOCAL_SERVER, typeof(IExtension).GUID, out var extensionObj);
|
||||
extensionPtr = Marshal.GetIUnknownForObject(extensionObj);
|
||||
if (hr < 0)
|
||||
{
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
}
|
||||
|
||||
_extensionObject = MarshalInterface<IExtension>.FromAbi(extensionPtr);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (extensionPtr != nint.Zero)
|
||||
{
|
||||
Marshal.Release(extensionPtr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void SignalDispose()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (IsRunning())
|
||||
{
|
||||
_extensionObject?.Dispose();
|
||||
}
|
||||
|
||||
_extensionObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
public IExtension? GetExtensionObject()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return IsRunning() ? _extensionObject : null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<T?> GetProviderAsync<T>()
|
||||
where T : class
|
||||
{
|
||||
await StartExtensionAsync();
|
||||
|
||||
return GetExtensionObject()?.GetProvider(_providerTypeMap[typeof(T)]) as T;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<T>> GetListOfProvidersAsync<T>()
|
||||
where T : class
|
||||
{
|
||||
await StartExtensionAsync();
|
||||
|
||||
var supportedProviders = GetExtensionObject()?.GetProvider(_providerTypeMap[typeof(T)]);
|
||||
if (supportedProviders is IEnumerable<T> multipleProvidersSupported)
|
||||
{
|
||||
return multipleProvidersSupported;
|
||||
}
|
||||
else if (supportedProviders is T singleProviderSupported)
|
||||
{
|
||||
return [singleProviderSupported];
|
||||
}
|
||||
|
||||
return Enumerable.Empty<T>();
|
||||
}
|
||||
|
||||
public void AddProviderType(ProviderType providerType) => _providerTypes.Add(providerType);
|
||||
|
||||
public bool HasProviderType(ProviderType providerType) => _providerTypes.Contains(providerType);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
GetPhysicallyInstalledSystemMemory
|
||||
GlobalMemoryStatusEx
|
||||
GetSystemInfo
|
||||
CoCreateInstance
|
||||
SetForegroundWindow
|
||||
IsIconic
|
||||
RegisterHotKey
|
||||
SetWindowLongPtr
|
||||
CallWindowProc
|
||||
ShowWindow
|
||||
SetForegroundWindow
|
||||
SetFocus
|
||||
SetActiveWindow
|
||||
MonitorFromWindow
|
||||
GetMonitorInfo
|
||||
SHCreateStreamOnFileEx
|
||||
CoAllowSetForegroundWindow
|
||||
SHCreateStreamOnFileEx
|
||||
SHLoadIndirectString
|
||||
@@ -0,0 +1,43 @@
|
||||
// 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 CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.UI.Pages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class ShellViewModel(IServiceProvider _serviceProvider) : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial bool IsLoaded { get; set; } = false;
|
||||
|
||||
[RelayCommand]
|
||||
public async Task<bool> LoadAsync()
|
||||
{
|
||||
var tlcManager = _serviceProvider.GetService<TopLevelCommandManager>();
|
||||
await tlcManager!.LoadBuiltinsAsync();
|
||||
IsLoaded = true;
|
||||
|
||||
// Built-ins have loaded. We can display our page at this point.
|
||||
var page = new MainListPage(_serviceProvider);
|
||||
WeakReferenceMessenger.Default.Send<NavigateToListMessage>(new(new(page!)));
|
||||
|
||||
// After loading built-ins, and starting navigation, kick off a thread to load extensions.
|
||||
tlcManager.LoadExtensionsCommand.Execute(null);
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await tlcManager.LoadExtensionsCommand.ExecutionTask!;
|
||||
if (tlcManager.LoadExtensionsCommand.ExecutionTask.Status != TaskStatus.RanToCompletion)
|
||||
{
|
||||
// TODO: Handle failure case
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// 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.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.CmdPal.Common.Services;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class TopLevelCommandManager(IServiceProvider _serviceProvider)
|
||||
{
|
||||
private IEnumerable<ICommandProvider>? _builtInCommands;
|
||||
|
||||
public ObservableCollection<TopLevelCommandWrapper> TopLevelCommands { get; set; } = [];
|
||||
|
||||
public async Task<bool> LoadBuiltinsAsync()
|
||||
{
|
||||
// Load built-In commands first. These are all in-proc, and
|
||||
// owned by our ServiceProvider.
|
||||
_builtInCommands = _serviceProvider.GetServices<ICommandProvider>();
|
||||
foreach (var provider in _builtInCommands)
|
||||
{
|
||||
CommandProviderWrapper wrapper = new(provider);
|
||||
await LoadTopLevelCommandsFromProvider(wrapper);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
|
||||
{
|
||||
await commandProvider.LoadTopLevelCommands();
|
||||
foreach (var i in commandProvider.TopLevelItems)
|
||||
{
|
||||
TopLevelCommands.Add(new(new(i)));
|
||||
}
|
||||
}
|
||||
|
||||
// Load commands from our extensions.
|
||||
// Currently, this
|
||||
// * queries the package catalog,
|
||||
// * starts all the extensions,
|
||||
// * then fetches the top-level commands from them.
|
||||
// TODO In the future, we'll probably abstract some of this away, to have
|
||||
// separate extension tracking vs stub loading.
|
||||
[RelayCommand]
|
||||
public async Task<bool> LoadExtensionsAsync()
|
||||
{
|
||||
var extensionService = _serviceProvider.GetService<IExtensionService>()!;
|
||||
var extensions = await extensionService.GetInstalledExtensionsAsync();
|
||||
foreach (var extension in extensions)
|
||||
{
|
||||
try
|
||||
{
|
||||
await extension.StartExtensionAsync();
|
||||
CommandProviderWrapper wrapper = new(extension);
|
||||
await LoadTopLevelCommandsFromProvider(wrapper);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// 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.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Abstraction of a top-level command. Currently owns just a live ICommandItem
|
||||
/// from an extension (or in-proc command provider), but in the future will
|
||||
/// also support stub top-level items.
|
||||
/// </summary>
|
||||
public partial class TopLevelCommandWrapper : ListItem
|
||||
{
|
||||
public ExtensionObject<ICommandItem> Model { get; }
|
||||
|
||||
public TopLevelCommandWrapper(ExtensionObject<ICommandItem> commandItem)
|
||||
: base(commandItem.Unsafe?.Command ?? new NoOpCommand())
|
||||
{
|
||||
// TODO: In reality, we should do an async fetch when we're created
|
||||
// from an extension object. Probably have an
|
||||
// `static async Task<TopLevelCommandWrapper> FromExtension(ExtensionObject<ICommandItem>)`
|
||||
// or a
|
||||
// `async Task PromoteStub(ExtensionObject<ICommandItem>)`
|
||||
Model = commandItem;
|
||||
try
|
||||
{
|
||||
var model = Model.Unsafe;
|
||||
if (model == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Title = model.Title;
|
||||
Subtitle = model.Subtitle;
|
||||
Icon = new(model.Icon.Icon);
|
||||
MoreCommands = model.MoreCommands;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +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 CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class ActionBarContextItemViewModel : ObservableObject
|
||||
{
|
||||
////private ICommand _command;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string Name { get; set; } = "Placeholder";
|
||||
|
||||
////private IconDataType Icon => Command.Icon;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool CanInvoke { get; set; } = true;
|
||||
|
||||
// TODO: do we want the icon here or get it over in the UI project?
|
||||
////[ObservableProperty]
|
||||
////private IconElement IcoElement => Microsoft.Terminal.UI.IconPathConverter.IconMUX(Icon.Icon);
|
||||
|
||||
public ActionBarContextItemViewModel(string name, bool canInvoke)
|
||||
{
|
||||
Name = name;
|
||||
CanInvoke = canInvoke;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class ActionBarViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial string ActionName { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool MoreCommandsAvailable { get; set; } = false;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<ActionBarContextItemViewModel> ContextActions { get; set; } = [];
|
||||
|
||||
public ActionBarViewModel()
|
||||
{
|
||||
// Just for fun
|
||||
ActionName = "My Action";
|
||||
MoreCommandsAvailable = true;
|
||||
ContextActions.Add(new ActionBarContextItemViewModel("Action1", true));
|
||||
ContextActions.Add(new ActionBarContextItemViewModel("Action2", true));
|
||||
}
|
||||
}
|
||||
@@ -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 CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Models;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class ListItemViewModel : ObservableObject
|
||||
{
|
||||
private readonly ExtensionObject<IListItem> _listItemModel;
|
||||
|
||||
public string Title => _listItemModel.Unsafe.Title;
|
||||
|
||||
public string Subtitle => _listItemModel.Unsafe.Subtitle;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path for the icon to load in the View layer. TODO: Converter/Cache
|
||||
/// </summary>
|
||||
public string IconUri => _listItemModel.Unsafe.Icon.Icon;
|
||||
|
||||
public ITag[] Tags => _listItemModel.Unsafe.Tags;
|
||||
|
||||
public ICommand Command => _listItemModel.Unsafe.Command;
|
||||
|
||||
public bool HasTags => Tags.Length > 0;
|
||||
|
||||
public ListItemViewModel(IListItem model)
|
||||
{
|
||||
_listItemModel = new(model);
|
||||
_listItemModel.Unsafe.PropChanged += Model_PropChanged;
|
||||
}
|
||||
|
||||
private void Model_PropChanged(object sender, PropChangedEventArgs args) => OnPropertyChanged(args.PropertyName);
|
||||
}
|
||||
@@ -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 CommunityToolkit.Mvvm.Collections;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class ListViewModel : ObservableObject
|
||||
{
|
||||
// Observable from MVVM Toolkit will auto create public properties that use INotifyPropertyChange change
|
||||
// https://learn.microsoft.com/dotnet/communitytoolkit/mvvm/observablegroupedcollections for grouping support
|
||||
[ObservableProperty]
|
||||
public partial ObservableGroupedCollection<string, ListItemViewModel> Items { get; set; } = [];
|
||||
|
||||
public ListViewModel(IListPage model)
|
||||
{
|
||||
// TEMPORARY: just plop all the items into a single group
|
||||
// see 9806fe5d8 for the last commit that had this with sections
|
||||
ObservableGroup<string, ListItemViewModel> group = new(string.Empty);
|
||||
|
||||
foreach (var item in model.GetItems())
|
||||
{
|
||||
group.Add(new(item));
|
||||
}
|
||||
|
||||
Items.AddGroup(group);
|
||||
}
|
||||
|
||||
// InvokeItemCommand is what this will be in Xaml due to source generator
|
||||
[RelayCommand]
|
||||
private void InvokeItem(ListItemViewModel item)
|
||||
{
|
||||
// TODO: we should probably just have the shell handle a "NavigateToCommand" message
|
||||
if (item.Command is IListPage listPage)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<NavigateToListMessage>(new(new(listPage)));
|
||||
}
|
||||
else
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<NavigateToDetailsMessage>(new(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
using Microsoft.CmdPal.Models;
|
||||
using Microsoft.CmdPal.UI.Pages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class ShellViewModel(IServiceProvider _serviceProvider) : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial bool IsLoaded { get; set; } = false;
|
||||
|
||||
public ObservableCollection<CommandProviderWrapper> ActionsProvider { get; set; } = [];
|
||||
|
||||
public ObservableCollection<ExtensionObject<IListItem>> TopLevelCommands { get; set; } = [];
|
||||
|
||||
private IEnumerable<ICommandProvider>? _builtInCommands;
|
||||
|
||||
[RelayCommand]
|
||||
public async Task<bool> LoadAsync()
|
||||
{
|
||||
_builtInCommands = _serviceProvider.GetServices<ICommandProvider>();
|
||||
|
||||
// Load Built In Commands First
|
||||
foreach (var provider in _builtInCommands)
|
||||
{
|
||||
CommandProviderWrapper wrapper = new(provider);
|
||||
ActionsProvider.Add(wrapper);
|
||||
|
||||
await LoadTopLevelCommandsFromProvider(wrapper);
|
||||
}
|
||||
|
||||
IsLoaded = true;
|
||||
|
||||
// TODO: would want to hydrate this from our services provider in the View layer, need to think about construction here...
|
||||
WeakReferenceMessenger.Default.Send<NavigateToListMessage>(new(new(new MainListPage(this))));
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
|
||||
{
|
||||
await commandProvider.LoadTopLevelCommands();
|
||||
foreach (var i in commandProvider.TopLevelItems)
|
||||
{
|
||||
TopLevelCommands.Add(new(new ListItem(i)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Application
|
||||
x:Class="Microsoft.CmdPal.UI.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
@@ -8,11 +8,11 @@
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
<!-- Other merged dictionaries here -->
|
||||
<!-- Other merged dictionaries here -->
|
||||
<ResourceDictionary Source="Styles/Button.xaml" />
|
||||
<ResourceDictionary Source="Styles/TextBox.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<!-- Other app resources here -->
|
||||
<!-- Other app resources here -->
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Common.Services;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks;
|
||||
using Microsoft.CmdPal.Ext.Calc;
|
||||
using Microsoft.CmdPal.Ext.Registry;
|
||||
@@ -12,6 +13,7 @@ using Microsoft.CmdPal.Ext.WindowsTerminal;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
@@ -29,6 +31,8 @@ public partial class App : Application
|
||||
/// </summary>
|
||||
public static new App Current => (App)Application.Current;
|
||||
|
||||
public Window? AppWindow { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IServiceProvider"/> instance to resolve application services.
|
||||
/// </summary>
|
||||
@@ -52,12 +56,10 @@ public partial class App : Application
|
||||
/// <param name="args">Details about the launch request and process.</param>
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
_window = new MainWindow();
|
||||
_window.Activate();
|
||||
AppWindow = new MainWindow();
|
||||
AppWindow.Activate();
|
||||
}
|
||||
|
||||
private Window? _window;
|
||||
|
||||
/// <summary>
|
||||
/// Configures the services for the application
|
||||
/// </summary>
|
||||
@@ -66,6 +68,9 @@ public partial class App : Application
|
||||
// TODO: It's in the Labs feed, but we can use Sergio's AOT-friendly source generator for this: https://github.com/CommunityToolkit/Labs-Windows/discussions/463
|
||||
ServiceCollection services = new();
|
||||
|
||||
// Root services
|
||||
services.AddSingleton(TaskScheduler.FromCurrentSynchronizationContext());
|
||||
|
||||
// Built-in Commands
|
||||
services.AddSingleton<ICommandProvider, BookmarksCommandProvider>();
|
||||
services.AddSingleton<ICommandProvider, CalculatorCommandProvider>();
|
||||
@@ -77,6 +82,10 @@ public partial class App : Application
|
||||
services.AddSingleton<ICommandProvider, RegistryCommandsProvider>();
|
||||
services.AddSingleton<ICommandProvider, WindowsSettingsCommandsProvider>();
|
||||
|
||||
// Models
|
||||
services.AddSingleton<TopLevelCommandManager>();
|
||||
services.AddSingleton<IExtensionService, ExtensionService>();
|
||||
|
||||
// ViewModels
|
||||
services.AddSingleton<ShellViewModel>();
|
||||
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="Microsoft.CmdPal.UI.Controls.ActionBar"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
xmlns:viewmodels="using:Microsoft.CmdPal.UI.ViewModels"
|
||||
Background="Transparent"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<converters:StringVisibilityConverter x:Key="StringNotEmptyToVisibilityConverter" EmptyValue="Collapsed" NotEmptyValue="Visible" />
|
||||
<converters:StringVisibilityConverter
|
||||
x:Key="StringNotEmptyToVisibilityConverter"
|
||||
EmptyValue="Collapsed"
|
||||
NotEmptyValue="Visible" />
|
||||
|
||||
<!-- Template for actions in the mode actions dropdown button -->
|
||||
<DataTemplate x:Key="ContextMenuViewModelTemplate" x:DataType="viewmodels:ActionBarContextItemViewModel">
|
||||
<DataTemplate x:Key="ContextMenuViewModelTemplate" x:DataType="viewmodels:CommandContextItemViewModel">
|
||||
<ListViewItem KeyDown="ActionListViewItem_KeyDown" Tapped="ActionListViewItem_Tapped">
|
||||
<Grid ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
@@ -30,80 +34,101 @@
|
||||
Grid.Column="0"
|
||||
Width="24"
|
||||
Height="24">
|
||||
<SymbolIcon Symbol="Emoji" />
|
||||
<!--<SymbolIcon Symbol="Emoji" />-->
|
||||
</ContentControl>
|
||||
</Viewbox>
|
||||
<TextBlock Grid.Column="1" Text="{x:Bind Name}" />
|
||||
<TextBlock Grid.Column="1" Text="{x:Bind Title}" />
|
||||
</Grid>
|
||||
</ListViewItem>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Remove all item animations from lists. They're entirely too slow to let that UI be usable. -->
|
||||
<Style x:Key="NoAnimationsPlease" TargetType="ListView">
|
||||
<Setter Property="ItemContainerTransitions">
|
||||
<Setter.Value>
|
||||
<TransitionCollection>
|
||||
<!-- (deleted transitions are left for reference for what we removed) -->
|
||||
<ContentThemeTransition />
|
||||
<!--<AddDeleteThemeTransition/>-->
|
||||
<!--<ReorderThemeTransition/>-->
|
||||
<!--<EntranceThemeTransition IsStaggeringEnabled="False"/>-->
|
||||
</TransitionCollection>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid Padding="8">
|
||||
<Grid Padding="8" ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Return label -->
|
||||
<StackPanel
|
||||
<!-- TO DO: Placeholder, needs to be replaced with extension info -->
|
||||
<Border
|
||||
Width="20"
|
||||
Height="20"
|
||||
Margin="12,0,0,0"
|
||||
Padding="6,2,4,3"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
BorderBrush="{ThemeResource ControlStrokeColorSecondaryBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4"
|
||||
Orientation="Horizontal"
|
||||
Visibility="{x:Bind ViewModel.ActionName, Converter={StaticResource StringNotEmptyToVisibilityConverter}, Mode=OneWay}">
|
||||
<TextBlock
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.ActionName, Mode=OneWay}" />
|
||||
<FontIcon
|
||||
Margin="4,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Glyph="" />
|
||||
</StackPanel>
|
||||
Background="{ThemeResource AccentFillColorDefaultBrush}"
|
||||
CornerRadius="{StaticResource ControlCornerRadius}" />
|
||||
|
||||
<SplitButton
|
||||
x:Name="MoreCommandsButton"
|
||||
Grid.Column="2"
|
||||
Margin="0,-2,0,0"
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Content="Actions"
|
||||
FontSize="12"
|
||||
Visibility="{x:Bind ViewModel.MoreCommandsAvailable, Mode=OneWay}">
|
||||
<SplitButton.Flyout>
|
||||
<Flyout Placement="TopEdgeAlignedRight">
|
||||
<ListView
|
||||
x:Name="ActionsDropdown"
|
||||
Margin="-12"
|
||||
ItemTemplate="{StaticResource ContextMenuViewModelTemplate}"
|
||||
ItemsSource="{x:Bind ViewModel.ContextActions, Mode=OneWay}"
|
||||
Style="{StaticResource NoAnimationsPlease}" />
|
||||
</Flyout>
|
||||
</SplitButton.Flyout>
|
||||
</SplitButton>
|
||||
Text="Plugin name" />
|
||||
<!-- TO DO: Replace with ItemsRepeater and bind "Primary commands"? -->
|
||||
<StackPanel
|
||||
Grid.Column="2"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal"
|
||||
Spacing="6">
|
||||
<Button
|
||||
Height="40"
|
||||
Padding="8,4,8,4"
|
||||
Visibility="{x:Bind ViewModel.PrimaryActionName, Converter={StaticResource StringNotEmptyToVisibilityConverter}, Mode=OneWay}">
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon Glyph="" />
|
||||
<StackPanel Orientation="Vertical" Spacing="2">
|
||||
<TextBlock
|
||||
FontSize="12"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.PrimaryActionName, Mode=OneWay}" />
|
||||
<FontIcon
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="10"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Glyph="" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button
|
||||
Height="40"
|
||||
Padding="8,4,8,4"
|
||||
Visibility="{x:Bind ViewModel.SecondaryActionName, Converter={StaticResource StringNotEmptyToVisibilityConverter}, Mode=OneWay}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon Glyph="" />
|
||||
<StackPanel Orientation="Vertical" Spacing="1">
|
||||
<TextBlock
|
||||
FontSize="12"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.SecondaryActionName, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
FontSize="9"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="Ctrl + 1" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button
|
||||
x:Name="MoreCommandsButton"
|
||||
Height="40"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=16}"
|
||||
Visibility="{x:Bind ViewModel.ShouldShowContextMenu, Mode=OneWay}">
|
||||
<Button.Flyout>
|
||||
<Flyout Placement="TopEdgeAlignedRight">
|
||||
<ListView
|
||||
x:Name="ActionsDropdown"
|
||||
Margin="-12"
|
||||
ItemTemplate="{StaticResource ContextMenuViewModelTemplate}"
|
||||
ItemsSource="{x:Bind ViewModel.ContextActions, Mode=OneWay}">
|
||||
<ListView.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
</ListView.ItemContainerTransitions>
|
||||
</ListView>
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -2,19 +2,23 @@
|
||||
// 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.Messaging;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
public sealed partial class ActionBar : UserControl
|
||||
public sealed partial class ActionBar : UserControl,
|
||||
IRecipient<UpdateActionBarMessage>
|
||||
{
|
||||
public ActionBarViewModel ViewModel { get; set; } = new();
|
||||
|
||||
public ActionBar()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
WeakReferenceMessenger.Default.Register<UpdateActionBarMessage>(this);
|
||||
}
|
||||
|
||||
private void ActionListViewItem_KeyDown(object sender, KeyRoutedEventArgs e)
|
||||
@@ -24,6 +28,18 @@ public sealed partial class ActionBar : UserControl
|
||||
|
||||
private void ActionListViewItem_Tapped(object sender, TappedRoutedEventArgs e)
|
||||
{
|
||||
// TODO
|
||||
MoreCommandsButton.Flyout.Hide();
|
||||
|
||||
if (sender is not ListViewItem listItem)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (listItem.DataContext is CommandContextItemViewModel item)
|
||||
{
|
||||
ViewModel?.InvokeItemCommand.Execute(item);
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(UpdateActionBarMessage message) => ViewModel.SelectedItem = message.ViewModel;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="Microsoft.CmdPal.UI.Controls.SearchBar"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<converters:StringVisibilityConverter x:Key="StringNotEmptyToVisibilityConverter" EmptyValue="Collapsed" NotEmptyValue="Visible" />
|
||||
<converters:BoolToVisibilityConverter x:Key="ReverseBoolToVisibilityConverter" TrueValue="Collapsed" FalseValue="Visible" />
|
||||
<converters:StringVisibilityConverter
|
||||
x:Key="StringNotEmptyToVisibilityConverter"
|
||||
EmptyValue="Collapsed"
|
||||
NotEmptyValue="Visible" />
|
||||
<converters:BoolToVisibilityConverter
|
||||
x:Key="ReverseBoolToVisibilityConverter"
|
||||
FalseValue="Visible"
|
||||
TrueValue="Collapsed" />
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
|
||||
<!-- Back button and search box -->
|
||||
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||
<Grid
|
||||
Margin="0,-10,0,0"
|
||||
Padding="0,12,0,12"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
@@ -43,7 +53,7 @@
|
||||
CornerRadius="16"
|
||||
FontSize="16"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
IsEnabled="{x:Bind Nested}"
|
||||
IsEnabled="{x:Bind Nested, Mode=OneWay}"
|
||||
Tapped="BackButton_Tapped"
|
||||
ToolTipService.ToolTip="Back"
|
||||
Visibility="{x:Bind Nested}">
|
||||
|
||||
@@ -20,7 +20,7 @@ public sealed partial class SearchBar : UserControl
|
||||
/// <summary>
|
||||
/// Gets the <see cref="DispatcherQueueTimer"/> that we create to track keyboard input and throttle/debounce before we make queries.
|
||||
/// </summary>
|
||||
private DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
|
||||
private readonly DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
|
||||
|
||||
public bool Nested { get; set; }
|
||||
|
||||
@@ -29,10 +29,7 @@ public sealed partial class SearchBar : UserControl
|
||||
this.InitializeComponent();
|
||||
}
|
||||
|
||||
private void BackButton_Tapped(object sender, TappedRoutedEventArgs e)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<NavigateBackMessage>();
|
||||
}
|
||||
private void BackButton_Tapped(object sender, TappedRoutedEventArgs e) => WeakReferenceMessenger.Default.Send<NavigateBackMessage>();
|
||||
|
||||
private void FilterBox_KeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
@@ -56,7 +53,8 @@ public sealed partial class SearchBar : UserControl
|
||||
}
|
||||
else if (e.Key == VirtualKey.Enter)
|
||||
{
|
||||
// TODO: ExecuteCommandMessage?
|
||||
WeakReferenceMessenger.Default.Send<ActivateSelectedListItemMessage>();
|
||||
|
||||
e.Handled = true;
|
||||
} // ctrl+k
|
||||
else if (ctrlPressed && e.Key == VirtualKey.K)
|
||||
@@ -79,7 +77,8 @@ public sealed partial class SearchBar : UserControl
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Clear the search box
|
||||
// Clear the search box
|
||||
FilterBox.Text = string.Empty;
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:Interactions="using:Microsoft.Xaml.Interactions.Core"
|
||||
xmlns:Interactivity="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
@@ -19,22 +21,28 @@
|
||||
IsSourceGrouped="True"
|
||||
Source="{x:Bind ViewModel.Items, Mode=OneWay}" />
|
||||
|
||||
<converters:StringVisibilityConverter
|
||||
x:Key="StringVisibilityConverter"
|
||||
EmptyValue="Collapsed"
|
||||
NotEmptyValue="Visible" />
|
||||
|
||||
<!-- https://learn.microsoft.com/windows/apps/design/controls/itemsview#specify-the-look-of-the-items -->
|
||||
<DataTemplate x:Key="ListItemViewModelTemplate" x:DataType="viewmodels:ListItemViewModel">
|
||||
|
||||
<!--TODO: collapse item if it's empty -->
|
||||
<ListViewItem MinHeight="56">
|
||||
<Grid ColumnSpacing="12">
|
||||
<!-- TODO: collapse item if it's empty -->
|
||||
<ListViewItem>
|
||||
<Grid Padding="0,10,0,10" ColumnSpacing="12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="28" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Viewbox Width="20"
|
||||
<Viewbox
|
||||
Width="20"
|
||||
Height="20"
|
||||
VerticalAlignment="Center">
|
||||
<!--TODO: Icon
|
||||
<!--TODO: Icon
|
||||
<ContentControl Content="{x:Bind IcoElement, Mode=OneWay}" />
|
||||
-->
|
||||
</Viewbox>
|
||||
@@ -58,10 +66,10 @@
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind Subtitle, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap" />
|
||||
<!--TODO: collapse subtitle if it's empty -->
|
||||
TextWrapping="NoWrap"
|
||||
Visibility="{x:Bind Subtitle, Mode=OneWay, Converter={StaticResource StringVisibilityConverter}}" />
|
||||
</StackPanel>
|
||||
<!--TODO: Add tags again -->
|
||||
<!-- TODO: Add tags again -->
|
||||
<!--<ItemsRepeater ItemTemplate="{StaticResource TagTemplate}"
|
||||
ItemsSource="{x:Bind Tags}"
|
||||
Grid.Column="2"
|
||||
@@ -74,26 +82,47 @@
|
||||
|
||||
<Grid>
|
||||
<!-- not using Interactivity:Interaction.Behaviors due to wanting to do AoT -->
|
||||
<!-- sticking with ListView as ItemsView doesn't have grouping built-in, could investigate coordinating
|
||||
keyboards between them and using ItemsRepeater for group headers, though that wouldn't use CVS either -->
|
||||
<ListView
|
||||
x:Name="ItemsList"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="ListView_ItemClick"
|
||||
ItemTemplate="{StaticResource ListItemViewModelTemplate}"
|
||||
ItemsSource="{x:Bind ItemsCVS.View, Mode=OneWay}">
|
||||
<ListView.GroupStyle>
|
||||
<GroupStyle HidesIfEmpty="True">
|
||||
<GroupStyle.HeaderTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock
|
||||
FontFamily="Segoe UI"
|
||||
FontSize="16"
|
||||
Text="{Binding Key}" />
|
||||
</DataTemplate>
|
||||
</GroupStyle.HeaderTemplate>
|
||||
</GroupStyle>
|
||||
</ListView.GroupStyle>
|
||||
</ListView>
|
||||
<!--
|
||||
sticking with ListView as ItemsView doesn't have grouping built-in, could investigate coordinating
|
||||
keyboards between them and using ItemsRepeater for group headers, though that wouldn't use CVS either
|
||||
-->
|
||||
<controls:SwitchPresenter TargetType="local:ViewModelLoadedState" Value="{x:Bind LoadedState, Mode=OneWay}">
|
||||
<controls:Case Value="Loaded">
|
||||
<ListView
|
||||
x:Name="ItemsList"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="ListView_ItemClick"
|
||||
SelectionChanged="ItemsList_SelectionChanged"
|
||||
ItemTemplate="{StaticResource ListItemViewModelTemplate}"
|
||||
ItemsSource="{x:Bind ItemsCVS.View, Mode=OneWay}">
|
||||
<ListView.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
</ListView.ItemContainerTransitions>
|
||||
<!--<ListView.GroupStyle>
|
||||
<GroupStyle HidesIfEmpty="True">
|
||||
<GroupStyle.HeaderTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock
|
||||
Margin="0,16,0,0"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{Binding Key, Mode=OneWay}" />
|
||||
</DataTemplate>
|
||||
</GroupStyle.HeaderTemplate>
|
||||
</GroupStyle>
|
||||
</ListView.GroupStyle>-->
|
||||
</ListView>
|
||||
</controls:Case>
|
||||
|
||||
<controls:Case IsDefault="True" Value="Loading">
|
||||
<ProgressRing
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
IsIndeterminate="True" />
|
||||
</controls:Case>
|
||||
|
||||
<controls:Case Value="Error">
|
||||
<TextBlock Text="Error loading List Page" />
|
||||
</controls:Case>
|
||||
</controls:SwitchPresenter>
|
||||
</Grid>
|
||||
</Page>
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
|
||||
@@ -15,28 +18,95 @@ namespace Microsoft.CmdPal.UI;
|
||||
/// </summary>
|
||||
public sealed partial class ListPage : Page,
|
||||
IRecipient<NavigateNextCommand>,
|
||||
IRecipient<NavigatePreviousCommand>
|
||||
IRecipient<NavigatePreviousCommand>,
|
||||
IRecipient<ActivateSelectedListItemMessage>
|
||||
{
|
||||
public ListViewModel? ViewModel { get; set; }
|
||||
private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
public ListViewModel? ViewModel
|
||||
{
|
||||
get => (ListViewModel?)GetValue(ViewModelProperty);
|
||||
set => SetValue(ViewModelProperty, value);
|
||||
}
|
||||
|
||||
// Using a DependencyProperty as the backing store for ViewModel. This enables animation, styling, binding, etc...
|
||||
public static readonly DependencyProperty ViewModelProperty =
|
||||
DependencyProperty.Register(nameof(ViewModel), typeof(ListViewModel), typeof(ListPage), new PropertyMetadata(null));
|
||||
|
||||
public ViewModelLoadedState LoadedState
|
||||
{
|
||||
get => (ViewModelLoadedState)GetValue(LoadedStateProperty);
|
||||
set => SetValue(LoadedStateProperty, value);
|
||||
}
|
||||
|
||||
// Using a DependencyProperty as the backing store for LoadedState. This enables animation, styling, binding, etc...
|
||||
public static readonly DependencyProperty LoadedStateProperty =
|
||||
DependencyProperty.Register(nameof(LoadedState), typeof(ViewModelLoadedState), typeof(ListPage), new PropertyMetadata(ViewModelLoadedState.Loading));
|
||||
|
||||
public ListPage()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
WeakReferenceMessenger.Default.Register<NavigateNextCommand>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigatePreviousCommand>(this);
|
||||
}
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
LoadedState = ViewModelLoadedState.Loading;
|
||||
if (e.Parameter is ListViewModel lvm)
|
||||
{
|
||||
ViewModel = lvm;
|
||||
if (!lvm.IsInitialized
|
||||
&& lvm.InitializeCommand != null)
|
||||
{
|
||||
ViewModel = null;
|
||||
lvm.InitializeCommand.Execute(null);
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await lvm.InitializeCommand.ExecutionTask!;
|
||||
|
||||
if (lvm.InitializeCommand.ExecutionTask.Status != TaskStatus.RanToCompletion)
|
||||
{
|
||||
// TODO: Handle failure case
|
||||
System.Diagnostics.Debug.WriteLine(lvm.InitializeCommand.ExecutionTask.Exception);
|
||||
|
||||
_ = _queue.EnqueueAsync(() =>
|
||||
{
|
||||
LoadedState = ViewModelLoadedState.Error;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = _queue.EnqueueAsync(() =>
|
||||
{
|
||||
ViewModel = lvm;
|
||||
LoadedState = ViewModelLoadedState.Loaded;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
ViewModel = lvm;
|
||||
LoadedState = ViewModelLoadedState.Loaded;
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterAll isn't AOT compatible
|
||||
WeakReferenceMessenger.Default.Register<NavigateNextCommand>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigatePreviousCommand>(this);
|
||||
WeakReferenceMessenger.Default.Register<ActivateSelectedListItemMessage>(this);
|
||||
|
||||
base.OnNavigatedTo(e);
|
||||
}
|
||||
|
||||
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
|
||||
{
|
||||
base.OnNavigatingFrom(e);
|
||||
|
||||
WeakReferenceMessenger.Default.Unregister<NavigateNextCommand>(this);
|
||||
WeakReferenceMessenger.Default.Unregister<NavigatePreviousCommand>(this);
|
||||
WeakReferenceMessenger.Default.Unregister<ActivateSelectedListItemMessage>(this);
|
||||
}
|
||||
|
||||
private void ListView_ItemClick(object sender, ItemClickEventArgs e)
|
||||
{
|
||||
if (e.ClickedItem is ListItemViewModel item)
|
||||
@@ -66,4 +136,27 @@ public sealed partial class ListPage : Page,
|
||||
ItemsList.ScrollIntoView(ItemsList.SelectedItem);
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(ActivateSelectedListItemMessage message)
|
||||
{
|
||||
if (ItemsList.SelectedItem is ListItemViewModel item)
|
||||
{
|
||||
ViewModel?.InvokeItemCommand.Execute(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void ItemsList_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (ItemsList.SelectedItem is ListItemViewModel item)
|
||||
{
|
||||
ViewModel?.UpdateSelectedItemCommand.Execute(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ViewModelLoadedState
|
||||
{
|
||||
Loaded,
|
||||
Loading,
|
||||
Error,
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user