mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-23 04:27:14 +01:00
Compare commits
13 Commits
dev/migrie
...
add-event-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7438d2b98 | ||
|
|
6c05e3966c | ||
|
|
2fb7d92e36 | ||
|
|
1963f3820c | ||
|
|
8e2123cfea | ||
|
|
a9205781d7 | ||
|
|
086e4b5676 | ||
|
|
089c5f8b50 | ||
|
|
113639a66c | ||
|
|
3c2cb4516a | ||
|
|
53c5e66cce | ||
|
|
4cde968c9b | ||
|
|
e148a89288 |
4
.github/actions/spell-check/expect.txt
vendored
4
.github/actions/spell-check/expect.txt
vendored
@@ -187,6 +187,7 @@ Canvascustomlayout
|
||||
CAPTUREBLT
|
||||
CAPTURECHANGED
|
||||
CARETBLINKING
|
||||
carlos
|
||||
Carlseibert
|
||||
CAtl
|
||||
caub
|
||||
@@ -206,6 +207,8 @@ certmgr
|
||||
cfp
|
||||
CHANGECBCHAIN
|
||||
changecursor
|
||||
chatasweetie
|
||||
checkmarks
|
||||
CHILDACTIVATE
|
||||
CHILDWINDOW
|
||||
CHOOSEFONT
|
||||
@@ -2145,6 +2148,7 @@ YSpeed
|
||||
YStr
|
||||
YTimer
|
||||
YVIRTUALSCREEN
|
||||
zamora
|
||||
ZEROINIT
|
||||
zonability
|
||||
zonable
|
||||
|
||||
@@ -71,6 +71,14 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.Uninstall_Success</td>
|
||||
<td>Logs when PowerToys is successfully uninstalled (who would do such a thing!).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.UpdateCheck_Completed</td>
|
||||
<td>Logs when an auto-update check completes, including success status, whether an update is available, and version information.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.UpdateDownload_Completed</td>
|
||||
<td>Logs when an update download completes, including success status and version.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### OOBE (Out-of-box experience)
|
||||
@@ -570,7 +578,7 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.FindMyMouse_MousePointerFocused</td>
|
||||
<td>Occurs when the mouse pointer is focused using Find My Mouse.</td>
|
||||
<td>Occurs when the mouse pointer is focused using Find My Mouse, including the activation method (double-tap left/right Ctrl, shake mouse, or shortcut).</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
|
||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.260107-build.2454" />
|
||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.260116-build.2514" />
|
||||
<PackageVersion Include="ControlzEx" Version="6.0.0" />
|
||||
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
|
||||
<PackageVersion Include="HelixToolkit.Core.Wpf" Version="2.24.0" />
|
||||
|
||||
@@ -218,10 +218,6 @@
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
<Deploy />
|
||||
</Project>
|
||||
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Microsoft.CmdPal.Ext.PerformanceMonitor.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Microsoft.CmdPal.Ext.PowerToys.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
@@ -669,6 +665,7 @@
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/LightSwitch/">
|
||||
<Project Path="src/modules/LightSwitch/LightSwitchLib/LightSwitchLib.vcxproj" Id="79267138-2895-4346-9021-21408d65379f" />
|
||||
<Project Path="src/modules/LightSwitch/LightSwitchModuleInterface/LightSwitchModuleInterface.vcxproj" Id="38177d56-6ad1-4adf-88c9-2843a7932166" />
|
||||
<Project Path="src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj" Id="08e71c67-6a7e-4ca1-b04e-2fb336410bac" />
|
||||
</Folder>
|
||||
|
||||
285
README.md
285
README.md
@@ -53,17 +53,17 @@ Go to the <a href="https://aka.ms/installPowerToys">PowerToys GitHub releases</a
|
||||
<!-- items that need to be updated release to release -->
|
||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.97%22
|
||||
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.96%22
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.1/PowerToysUserSetup-0.96.1-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.1/PowerToysUserSetup-0.96.1-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.1/PowerToysSetup-0.96.1-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.1/PowerToysSetup-0.96.1-arm64.exe
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.0/PowerToysUserSetup-0.97.0-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.0/PowerToysUserSetup-0.97.0-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.0/PowerToysSetup-0.97.0-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.0/PowerToysSetup-0.97.0-arm64.exe
|
||||
|
||||
| Description | Filename |
|
||||
|----------------|----------|
|
||||
| Per user - x64 | [PowerToysUserSetup-0.96.1-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.96.1-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.96.1-x64.exe][ptMachineX64] |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.96.1-arm64.exe][ptMachineArm64] |
|
||||
| Per user - x64 | [PowerToysUserSetup-0.97.0-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.97.0-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.97.0-x64.exe][ptMachineX64] |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.97.0-arm64.exe][ptMachineArm64] |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -103,134 +103,193 @@ There are <a href="https://learn.microsoft.com/windows/powertoys/install#communi
|
||||
</details>
|
||||
|
||||
## ✨ What's new
|
||||
**Version 0.96 (November 2025)**
|
||||
**Version 0.97 (January 2026)**
|
||||
|
||||
For an in-depth look at the latest changes, visit the [Windows Command Line blog](https://aka.ms/powertoys-releaseblog).
|
||||
|
||||
**✨ Highlights**
|
||||
- Advanced Paste now supports multiple online and on-device AI model providers: Azure OpenAI, OpenAI, Google Gemini, Mistral, Foundry Local and Ollama.
|
||||
- Command Palette received extensive improvements including file search filters, better clipboard history metadata, context-menu styling, and dozens of bug fixes and enhancements.
|
||||
- PowerRename can now extract and use photo metadata (EXIF, XMP) in renaming patterns like `%Camera`, `%Lens`, and `%ExposureTime`.
|
||||
- **Command Palette**: Major expansion with PowerToys extension, Remote Desktop built-in extension, theme customization, drag-and-drop support, fallback ranking controls, sections/separators for pages, pinyin Chinese matching, and many UX refinements
|
||||
- **Settings**: Quick Access flyout is now a standalone process for significantly faster startup, theme-adaptive tray icon, AOT serialization, and multiple UI/accessibility fixes
|
||||
- **CursorWrap (New!)**: New mouse utility that lets your cursor wrap around screen edges, making multi-monitor navigation faster and more seamless.
|
||||
- **Advanced Paste**: Image input for AI, color detection in clipboard history, Foundry Local improvements, Azure AI icons, and multiple bug fixes
|
||||
- **CLI Support Expanded**: FancyZones, Image Resizer, and File Locksmith can now be controlled from the command line for layout management, batch image resizing, and file lock inspection.
|
||||
- **LightSwitch**: Added support for automatically following Windows Night Light mode.
|
||||
- **Release Experience & Quality**: Refreshed "What’s new" dialog, plus many performance improvements, stability fixes, and refinements across PowerToys.
|
||||
|
||||
### Advanced Paste
|
||||
- Advanced Paste now lets you connect to multiple AI providers instead of being limited to a single OpenAI provider. See [Advanced Paste documentation](https://learn.microsoft.com/windows/powertoys/advanced-paste) for usage.
|
||||
## Advanced Paste
|
||||
|
||||
### Awake
|
||||
- The Awake countdown timer now stays accurate over long periods. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Fixed Awake context menu positioning. The fix removed the conversion of the mouse cursor from screen to client-window coordinates, instead using the raw screen coordinates returned by GetCursorPos; the context menu now appears at the correct screen position. Thanks [@lzandman](https://github.com/lzandman)!
|
||||
- Added hex color previews in clipboard history. Thanks [@crramirez](https://github.com/crramirez)!
|
||||
- Added automatic placeholder endpoints when required fields are left empty.
|
||||
- Fixed a grammar issue in the AI settings description. Thanks [@erik-anderson](https://github.com/erik-anderson)!
|
||||
- Fixed loading order so custom action hotkeys are read correctly.
|
||||
- Updated Advanced Paste descriptions to reflect support for online and local models.
|
||||
- Fixed clipboard history item selection so it doesn’t duplicate entries.
|
||||
- Prevented placeholder endpoints from being saved for providers that don’t need them.
|
||||
- Added image input support for AI transforms and improved clipboard change tracking.
|
||||
|
||||
### Command Palette
|
||||
- The search field in context menus now matches the look of the Command Palette, with a smoke backdrop and improved padding.
|
||||
- Fallback items such as math calculations or the Run command now appear in results more quickly. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Ensured the command bar updates correctly after navigating to another page and commands are displayed correctly. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- The Command Palette settings page has been reorganized. Activation-key options are grouped under an expander and extension settings are framed for improved readability.
|
||||
- When you modify a command, its alias, hotkey, and tags now update in the top-level list, keeping the displayed information in sync. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Press `Ctrl + ,` to open Command Palette settings from anywhere. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- You can use `Page Up` and `Page Down` to navigate the list while focus is in the search box. Thanks [@samrueby](https://github.com/samrueby)!
|
||||
- Fixed an issue where the search box could disappear when navigating pages. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Ensured search text is selected when *Go home when activated* and *Highlight search on activate* are both enabled. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed an issue where Command Palette window occasionally appeared on the taskbar under certain Windows settings. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Ensured that labels and icons of list items and menu items update when they change. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed visibility of list filters when navigating to a content page. Thanks [@DevLGuilherme](https://github.com/DevLGuilherme)!
|
||||
- Added search to the extension list and a link to extensions on the Microsoft Store. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added options to open the Command Palette window at its last position or re-center it.
|
||||
- The Command Palette now remembers its window size after restarting.
|
||||
- Added a global error handler that logs fatal errors and provides feedback when unexpected failures force Command Palette to close. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed forms and extension settings not showing on some machines due to a missing VC++ runtime.
|
||||
- Restored ranking of fallback commands for built-in extensions (Sleep, Shutdown, Windows settings, Web search, etc.). Thanks [@jiripolasek](https://github.com/jiripolasek).
|
||||
- Improved and unified labels and texts across the application!
|
||||
- Maintainance: Resolved numerous build warnings in Command Palette projects; no user-visible impact. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Maintainance: Fixed a logging issue so exception messages are properly recorded instead of placeholder text, improving troubleshooting. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
## Awake
|
||||
|
||||
### Command Palette Extensions
|
||||
- Bookmarks: Added hints about bookmark placeholders to the Add/Edit Bookmark form. — Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Bookmarks: Improved migration of bookmarks from older versions and fixed an issue where aliases or keyboard shortcuts could be lost after restart. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Clipboard history: Items shown in Command Palette’s clipboard history now include helpful metadata. For example, image items show dimensions, text files show names and sizes, web links include page titles, and text entries display word counts. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- File search: Added filter buttons to show *all items*, *files only*, or *folders only*. Selecting a filter adds `kind:folders` or `kind:not folders` to narrow results.
|
||||
- System commands: Replaced the `:red_circle:` placeholder with an actual red-circle emoji so the correct icon appears in the UI. Thanks [@samrueby](https://github.com/samrueby)!
|
||||
- WinGet: Search performance feels more responsive because typed input is now processed via a task queue rather than complex cancellation tokens!
|
||||
- Window Walker: UWP apps no longer show a "not responding" label when suspended. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Window Walker: Now displays the actual icon of each window rather than using the process icon, improving recognition of PWAs and Python GUIs. Thanks [@Lee-WonJun](https://github.com/Lee-WonJun)!
|
||||
- Windows Terminal profiles: Fixed a rare crash in the Windows Terminal extension when the `LOCALAPPDATA` environment variable was missing. The path is now retrieved via a reliable API. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed Awake CLI so help, errors, and logs appear correctly in the console. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
|
||||
### Find My Mouse
|
||||
- Activating Find My Mouse no longer makes the cursor change to the busy (hourglass) icon or steals focus from your active application.
|
||||
## Command Palette
|
||||
|
||||
### Hosts File Editor
|
||||
- Added customizable backup settings allowing users to configure backup frequency, location, and auto-deletion policies. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- Fixed background image loading in BlurImageControl. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed SDK packaging paths and added a CI SDK build stage.
|
||||
- Aligned naming and spell-checking with .NET conventions. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added drag-and-drop support for Command Palette items. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added a PowerToys Command Palette extension to discover and launch PowerToys utilities.
|
||||
- Fixed grid view bindings and layout issues. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed a line-break issue in RDC extension toast messages. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Made the Settings button text localizable. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Hid the RDC fallback on the home page and fixed MSTSC working directory handling. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Optimized result list merging for better performance. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Added Small/Medium/Large detail sizes in the extensions API. Thanks [@DevLGuilherme](https://github.com/DevLGuilherme)!
|
||||
- Hid fallback commands on the home page when no query is entered. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added back navigation support in the Settings window. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added a Command Palette solution filter. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Updated Extension SDK documentation links to Microsoft Learn. Thanks [@RubenFricke](https://github.com/RubenFricke)!
|
||||
- Added a custom search engine URL setting for Web Search. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added pinyin matching for Chinese input. Thanks [@frg2089](https://github.com/frg2089)!
|
||||
- Bumped Command Palette version to 0.8.
|
||||
- Removed subtitles from built-in top-level commands. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Refined separator styling in the details pane. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added a built-in Remote Desktop extension.
|
||||
- Added a Peek command to the Indexer extension.
|
||||
- Improved default browser detection using the Windows Shell API. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added Escape key behavior options. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added theme and background customization options. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Improved WinGet package app matching. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added an auto-return-home delay setting. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added fallback ranking and global results settings.
|
||||
- Removed the selection indicator in the context menu list. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added a developer ribbon with build and log info. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Updated the “Learn more” string for Command Palette. Thanks [@pratnala](https://github.com/pratnala)!
|
||||
- Added arrow-key navigation for grid views. Thanks [@samrueby](https://github.com/samrueby)!
|
||||
- Fixed version display when running unpackaged. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added a native debugging launch profile. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Reduced redundant property change notifications in the SDK. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Improved section readability and accessibility. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Made gallery spacing uniform. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added sections and separators for list and grid pages. Thanks [@DevLGuilherme](https://github.com/DevLGuilherme)!
|
||||
|
||||
### Image Resizer
|
||||
- Fixed settings consistency during batch resize operations by capturing settings once before processing. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
## Crop & Lock
|
||||
|
||||
### Light Switch
|
||||
- Introduced new UI to allow users to manually enter their latitude and longitude in Sunrise to Sunset mode.
|
||||
- Refactored service with cleaner state management for stability.
|
||||
- Removed logs from every tick, only logging key events to largely reduce log size.
|
||||
- Added a screenshot mode that freezes a cropped region into its own window. Thanks [@fm-sys](https://github.com/fm-sys)!
|
||||
|
||||
### Mouse Pointer Crosshairs
|
||||
- Enabled switching between Mouse Pointer Crosshairs and Gliding Cursor modes. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
## Cursor Wrap
|
||||
|
||||
### Mouse Without Borders
|
||||
- Added horizontal scrolling support. Thanks [@MasonBergstrom](https://github.com/MasonBergstrom)!
|
||||
- Improved Cursor Wrap behavior on multi-monitor setups by wrapping only at outer edges. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
|
||||
### Peek
|
||||
- Fixed media files remaining locked after preview window closes. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Added a command-line interface for file previewing. See the [Peek documentation](https://learn.microsoft.com/windows/powertoys/peek) for usage. Thanks [@prochan2](https://github.com/prochan2)!
|
||||
## FancyZones
|
||||
|
||||
### PowerRename
|
||||
- PowerRename no longer crashes due to a missing resources file.
|
||||
- Added photo metadata extraction support using EXIF and XMP for pattern-based renaming with camera info, GPS coordinates, and date taken. See [PowerRename Documentation](https://learn.microsoft.com/en-us/windows/powertoys/powerrename).
|
||||
- Fixed editor overlay positioning on mixed-DPI multi-monitor setups. Thanks [@Memphizzz](https://github.com/Memphizzz)!
|
||||
- Added a FancyZones CLI for command-line layout management.
|
||||
|
||||
### PowerToys Run
|
||||
- Added retry logic with exponential backoff to handle DWM composition errors during theme changes. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Updated OneNote icons to reflect new Microsoft 365 design. Thanks [@trevorNgo](https://github.com/trevorNgo)!
|
||||
## File Locksmith
|
||||
|
||||
### Quick Accent
|
||||
- Added diameter symbol (⌀) for Shift+O in Special Characters mode, thanks to [@anselumjuju](https://github.com/anselumjuju)!
|
||||
- Added a File Locksmith CLI for querying, waiting on, or killing file locks.
|
||||
|
||||
### Zoomit
|
||||
- Smoothed out zoom-animation in ZoomIt by coalescing mouse-move and timer events, thanks to [@foxmsft](https://github.com/foxmsft)!
|
||||
- Enabled GIF support for ZoomIt, thanks to [@MarioHewardt](https://github.com/MarioHewardt)!
|
||||
- Fixed spelling mistakes, and refactored some literal strings to string constants, thanks to [@lzandman](https://github.com/lzandman)!
|
||||
- Fixed inaccurate "actual size" screenshots in ZoomIt and resolves a GDI handle leak, improving capture fidelity and long-session stability. thanks to [@daverayment](https://github.com/daverayment)!
|
||||
## Find My Mouse
|
||||
|
||||
### Settings
|
||||
- Fixed title bar overlapping issue at smaller window sizes.
|
||||
- Refined shortcut control visual design with improved consistency and spacing.
|
||||
- Added dashboard utilities sorting by name or status.
|
||||
- Made update notification InfoBar in flyout clickable for direct navigation to update page.
|
||||
- Expanded installation instructions by default in README.
|
||||
- Improved accessibility for shortcut conflict button with static resource-based automation properties.
|
||||
- Added ScrollViewer to Command Palette page in PowerToys Settings. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed module list glitches and Sort Status checkmark issue. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Improved spotlight edge rendering for clearer Find My Mouse visuals.
|
||||
- Added telemetry to track how Find My Mouse is triggered.
|
||||
|
||||
### Development
|
||||
- Fixed accessibility by associating controls with labels for screen readers.
|
||||
- Added accessible name to Shortcut Conflicts button for screen readers.
|
||||
- Excluded TitleBars from tab navigation across multiple utilities. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Migrated build infrastructure from Windows Server 2019 to Server 2022 with improved failure logging and predictable NuGet package paths.
|
||||
- Configured build agents to use larger P: drive for release builds to address disk space constraints.
|
||||
- Enhanced DSC v3 support by organizing resource manifests in a dedicated subfolder with PATH configuration.
|
||||
- Reduced installer bundle size by 6-7MB through centralized Hybrid CRT configuration across all C++ projects.
|
||||
- Updated .NET packages to version 9.0.10 for security fixes. Thanks [@snickler](https://github.com/snickler)!
|
||||
- Fixed spell check dictionary entries for consistency.
|
||||
- Restored accidentally deleted NuGet configuration file for Command Palette extensions.
|
||||
- Fixed package identity build by updating AppxManifest entry points to use PowerShell Core.
|
||||
- Optimized CI pipeline by replacing file copy operations with hard links and moves, reducing build time and disk usage by 10-15GB.
|
||||
- Updated Copilot guidance and PR prompt workflow.
|
||||
- Included high-volume bugs in issue template header. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Fixed incorrect HRESULT logging for inner exceptions. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Introduced shared sparse package identity for PowerToys Win32 components to enable access to Windows platform APIs.
|
||||
- Consolidated installer builds to produce both machine and user installers simultaneously, reducing build time and complexity.
|
||||
- Migrated exclusively to WiX v5 installer infrastructure, removing legacy WiX v3 support.
|
||||
- Temporarily removed PowerToys installer path from PATH environment variable to prevent application crashes.
|
||||
- Added complete OCR UI test coverage with automated tests for activation, settings, language selection, and text extraction.
|
||||
- Fixed test input for drive path normalization in bookmark resolver unit tests.
|
||||
- Fixed Peek UI tests by restoring Ctrl+Space activation shortcut for test scenarios.
|
||||
- Hided apps in PowerToys.SpareApps package from Start Menu. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
## Image Resizer
|
||||
|
||||
- Fixed Fill mode cropping when Shrink Only is enabled. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Added a dedicated Image Resizer CLI for scripted resizing.
|
||||
|
||||
## Light Switch
|
||||
|
||||
- Added telemetry events for Light Switch usage and settings changes.
|
||||
- Added a Follow Night Light mode to sync theme changes with Night Light.
|
||||
- Clarified LightSwitchService and LightSwitchStateManager roles in docs.
|
||||
- Added a Quick Access dashboard button to toggle Light Switch quickly.
|
||||
- Ensured Light Switch honors GPO policy states with clear status messaging.
|
||||
|
||||
## Mouse Without Borders
|
||||
|
||||
- Continued refactoring Mouse Without Borders by splitting the large Common class into focused components. Thanks [@mikeclayton](https://github.com/mikeclayton)!
|
||||
- Completed the Common class refactor with Core and IPC helper extraction. Thanks [@mikeclayton](https://github.com/mikeclayton)!
|
||||
|
||||
## Peek
|
||||
|
||||
- Hardened Peek previews with strict resource filtering and safer external link warnings.
|
||||
- Improved SVG preview compatibility by rendering via WebView2.
|
||||
|
||||
## PowerRename
|
||||
|
||||
- Added HEIF/AVIF EXIF metadata extraction and extension status guidance for related previews.
|
||||
- Fixed undefined behavior in file time handling. Thanks [@safocl](https://github.com/safocl)!
|
||||
- Optimized memory allocation for depth-based rename processing.
|
||||
- Fixed Unicode normalization and non‑breaking space matching. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Fixed date token replacements followed by capital letters. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
|
||||
## PowerToys Run Plugins
|
||||
|
||||
- Fixed a plugin name typo and added Project Launcher to the third‑party list. Thanks [@artickc](https://github.com/artickc)!
|
||||
- Added the Open With Antigravity plugin to the third‑party list. Thanks [@artickc](https://github.com/artickc)!
|
||||
|
||||
## PowerToys Run
|
||||
|
||||
- Avoided unnecessary hotkey conflict checks when settings change.
|
||||
- Added QuickAI to the third-party PowerToys Run plugin list. Thanks [@ruslanlap](https://github.com/ruslanlap)!
|
||||
|
||||
## Quick Accent
|
||||
|
||||
- Added localized quotation marks to Quick Accent. Thanks [@warquys](https://github.com/warquys)!
|
||||
- Fixed duplicate and redundant characters in Quick Accent sets. Thanks [@noraa-junker](https://github.com/noraa-junker)!
|
||||
- Fixed DPI positioning issues for Quick Accent on mixed-DPI setups. Thanks [@noraa-junker](https://github.com/noraa-junker)!
|
||||
|
||||
## Settings
|
||||
|
||||
- Added a new tray icon that adapts to theme changes. Thanks [@HO-COOH](https://github.com/HO-COOH)!
|
||||
- Centralized module enable/disable logic for cleaner Settings UI updates.
|
||||
- Simplified Settings utilities by removing ISettingsUtils/ISettingsPath interfaces. Thanks [@noraa-junker](https://github.com/noraa-junker)!
|
||||
- Improved Settings UI consistency and disabled-state visuals.
|
||||
- Added semantic headings to the Dashboard for better accessibility.
|
||||
- Introduced Quick Access as a standalone host with updated Settings integration.
|
||||
- Fixed Dashboard toggle flicker and sort menu checkmarks. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Added Native AOT-compatible settings serialization.
|
||||
- Standardized mouse tool description text. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Added a global SettingsUtils singleton to reduce repeated initialization.
|
||||
|
||||
## Development
|
||||
|
||||
- Fixed broken devdocs links to the coding style guide. Thanks [@RubenFricke](https://github.com/RubenFricke)!
|
||||
- Migrated main and installer solutions to .slnx for improved build tooling.
|
||||
- Restored local installer builds after the WiX v5 upgrade with signing and versioning fixes.
|
||||
- Added incremental review tooling and structured AI prompts for PR/issue reviews.
|
||||
- Documented bot commands and cleaned up devdocs structure. Thanks [@noraa-junker](https://github.com/noraa-junker)!
|
||||
- Updated WinAppSDK pipeline defaults to 1.8 and fixed restore handling.
|
||||
- Updated the COMMUNITY list to reflect current roles.
|
||||
- Maintained community member ordering and added a new entry.
|
||||
- Re-enabled centralized PackageReference for native projects with VS auto-restore.
|
||||
- Disabled MSBuild caching by default in CI to avoid build instability.
|
||||
- Updated the latest WinAppSDK daily pipeline for split-dependency restores.
|
||||
- Suppressed experimental build warnings and aligned WrapPanel stretch handling.
|
||||
- Reordered the spell-check expect list for consistent automation.
|
||||
- Migrated native projects to centralized PackageReference management.
|
||||
- Cleaned spell-check dictionary entries and capitalization.
|
||||
- Synced commit/PR prompts and wired VS Code to repo prompt files.
|
||||
- Added VS Code build tasks and improved build script path handling.
|
||||
- Updated Windows App SDK package versions in central package management.
|
||||
- Migrated cmdpal extension native project to PackageReference and fixed outputs.
|
||||
- Reverted PackageReference changes back to packages.config where needed.
|
||||
- Bypassed a release version check for a failing DLL to keep pipelines green.
|
||||
- Consolidated Copilot instructions and fixed prompt frontmatter.
|
||||
- Added signing entries for new Quick Access binaries and CLI version metadata.
|
||||
- Fixed install scope detection to avoid mixed per-user/per-machine installs.
|
||||
- Added a Module Loader tool to quickly test PowerToys modules without full builds. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
- Added update telemetry to understand auto-update checks and downloads.
|
||||
- Updated the telemetry package for new compliance requirements. Thanks [@carlos-zamora](https://github.com/carlos-zamora)!
|
||||
- Documented missing telemetry events in DATA_AND_PRIVACY.
|
||||
- Fixed UI test pipeline restores for .slnx solutions.
|
||||
- Added UI automation coverage for Advanced Paste clipboard history flows.
|
||||
- Stabilized FancyZones UI tests with more reliable selectors and screen recordings.
|
||||
|
||||
## 🛣️ Roadmap
|
||||
We are planning some nice new features and improvements for the next releases – a revamped Keyboard Manager UI, custom endpoint and local model support for Advanced Paste, Command Palette improvements and a brand-new Shortcut Guide experience! Stay tuned for [v0.96][github-next-release-work]!
|
||||
We are planning some nice new features and improvements for the next releases – PowerDisplay, Command Palette improvements and a brand-new Shortcut Guide experience! Stay tuned for [v0.97][github-next-release-work]!
|
||||
|
||||
## ❤️ PowerToys Community
|
||||
The PowerToys team is extremely grateful to have the [support of an amazing active community][community-link]. The work you do is incredibly important. PowerToys wouldn't be nearly what it is today without your help filing bugs, updating documentation, guiding the design, or writing features. We want to say thank you and take time to recognize your work. Your contributions and feedback improve PowerToys month after month!
|
||||
|
||||
197
doc/devdocs/events.md
Normal file
197
doc/devdocs/events.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# Telemetry Events
|
||||
|
||||
PowerToys collects limited telemetry to understand feature usage, reliability, and product quality. When adding a new telemetry event, follow the steps below to ensure the event is properly declared, documented, and available after release.
|
||||
|
||||
**⚠️ Important**: Telemetry must never include personal information, file paths, or user‑generated content.
|
||||
|
||||
## Developer Effort Overview (What to Expect)
|
||||
|
||||
Adding a telemetry event is a **multi-step process** that typically spans several areas of the codebase and documentation.
|
||||
|
||||
At a high level, developers should expect to:
|
||||
|
||||
1. Within one PR:
|
||||
1. Add a new telemetry event(s) to module
|
||||
1. Add the new event(s) DATA_AND_PRIVACY.md
|
||||
1. Reach out to @carlos-zamora or @chatasweetie so internal scripts can process new event(s)
|
||||
|
||||
### Privacy Guidelines
|
||||
|
||||
**NEVER** log:
|
||||
|
||||
- User data (text, files, emails, etc.)
|
||||
- File paths or filenames
|
||||
- Personal information
|
||||
- Sensitive system information
|
||||
- Anything that could identify a specific user
|
||||
|
||||
DO log:
|
||||
|
||||
- Feature usage (which features, how often)
|
||||
- Success/failure status
|
||||
- Timing/performance metrics
|
||||
- Error types (not error messages with user data)
|
||||
- Aggregate counts
|
||||
|
||||
### Event Naming Convention
|
||||
|
||||
Follow this pattern: `UtilityName_EventDescription`
|
||||
|
||||
Examples:
|
||||
|
||||
- `ColorPicker_Session`
|
||||
- `FancyZones_LayoutApplied`
|
||||
- `PowerRename_Rename`
|
||||
- `AdvancedPaste_FormatClicked`
|
||||
- `CmdPal_ExtensionInvoked`
|
||||
|
||||
## Adding Telemetry Events to PowerToys
|
||||
|
||||
PowerToys uses ETW (Event Tracing for Windows) for telemetry in both C++ and C# modules. The telemetry system is:
|
||||
|
||||
- Opt-in by default (disabled since v0.86)
|
||||
- Privacy-focused - never logs personal info, file paths, or user-generated content
|
||||
- Controlled by registry - HKEY_CURRENT_USER\Software\Classes\PowerToys\AllowDataDiagnostics
|
||||
|
||||
### C++ Telemetry Implementation
|
||||
|
||||
**Core Components**
|
||||
|
||||
| File | Purpose |
|
||||
| ------------- |:-------------:|
|
||||
| [ProjectTelemetry.h](../../src/common/Telemetry/ProjectTelemetry.h) | Declares the global ETW provider g_hProvider |
|
||||
| [TraceBase.h](../../src/common/Telemetry/TraceBase.h) | Base class with RegisterProvider(), UnregisterProvider(), and IsDataDiagnosticsEnabled() check |
|
||||
| [TraceLoggingDefines.h](../../src/common/Telemetry/TraceLoggingDefines.h) | Privacy tags and telemetry option group macros
|
||||
|
||||
|
||||
#### Pattern for C++ Modules
|
||||
|
||||
1. Create a `Trace` class inheriting from `telemetry::TraceBase` (src/common/Telemetry/TraceBase.h):
|
||||
|
||||
```c
|
||||
// trace.h
|
||||
#pragma once
|
||||
#include <common/Telemetry/TraceBase.h>
|
||||
|
||||
class Trace : public telemetry::TraceBase
|
||||
{
|
||||
public:
|
||||
static void MyEvent(/* parameters */);
|
||||
};
|
||||
```
|
||||
|
||||
2. Implement events using `TraceLoggingWriteWrapper`:
|
||||
|
||||
```cpp
|
||||
// trace.cpp
|
||||
#include "trace.h"
|
||||
#include <common/Telemetry/TraceBase.h>
|
||||
|
||||
TRACELOGGING_DEFINE_PROVIDER(
|
||||
g_hProvider,
|
||||
"Microsoft.PowerToys",
|
||||
(0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
|
||||
TraceLoggingOptionProjectTelemetry());
|
||||
|
||||
void Trace::MyEvent(bool enabled)
|
||||
{
|
||||
TraceLoggingWriteWrapper(
|
||||
g_hProvider,
|
||||
"ModuleName_EventName", // Event name
|
||||
TraceLoggingBoolean(enabled, "Enabled"), // Event data
|
||||
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
|
||||
TraceLoggingBoolean(TRUE, "UTCReplace_AppSessionGuid"),
|
||||
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
|
||||
}
|
||||
```
|
||||
|
||||
**Key C++ Telemetry Macros**
|
||||
|
||||
| Macro | Purpose |
|
||||
| ------------- |:-------------:|
|
||||
| `TraceLoggingWriteWrapper` [CustomAction.cpp](../../installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp) | Wraps `TraceLoggingWrite` with `IsDataDiagnosticsEnabled()` check |
|
||||
| `ProjectTelemetryPrivacyDataTag(tag)` [TraceLoggingDefines.h](../../src/common/Telemetry/TraceLoggingDefines.h) | Sets privacy classification |
|
||||
|
||||
### C# Telemetry Implementation
|
||||
|
||||
**Core Components**
|
||||
|
||||
| File | Purpose |
|
||||
| ------------- |:-------------:|
|
||||
| [PowerToysTelemetry.cs](../../src/common/ManagedTelemetry/Telemetry/PowerToysTelemetry.cs) | Singleton `Log` instance with `WriteEvent<T>()` method |
|
||||
| [EventBase.cs](../../src/common/ManagedTelemetry/Telemetry/Events/EventBase.cs) | Base class for all events (provides `EventName`, `Version`) |
|
||||
| [IEvent.cs](../../src/common/ManagedTelemetry/Telemetry/Events/IEvent.cs) | Interface requiring `PartA_PrivTags` property |
|
||||
| [TelemetryBase.cs](../../src/common/Telemetry/TelemetryBase.cs) | Inherits from `EventSource`, defines ETW constants |
|
||||
| [DataDiagnosticsSettings.cs](../../src/common/ManagedTelemetry/Telemetry/DataDiagnosticsSettings.cs) | Registry-based enable/disable check
|
||||
|
||||
#### Pattern for C# Modules
|
||||
|
||||
1. Create an event class inheriting from `EventBase` and implementing `IEvent`:
|
||||
|
||||
```csharp
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Diagnostics.Tracing;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.PowerToys.Telemetry.Events;
|
||||
|
||||
namespace MyModule.Telemetry
|
||||
{
|
||||
[EventData]
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
|
||||
public class MyModuleEvent : EventBase, IEvent
|
||||
{
|
||||
// Event properties (logged as telemetry data)
|
||||
public string SomeProperty { get; set; }
|
||||
public int SomeValue { get; set; }
|
||||
|
||||
// Required: Privacy tag
|
||||
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
|
||||
|
||||
// Optional: Set EventName in constructor (defaults to class name)
|
||||
public MyModuleEvent(string prop, int val)
|
||||
{
|
||||
EventName = "MyModule_EventName";
|
||||
SomeProperty = prop;
|
||||
SomeValue = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. Log the event:
|
||||
|
||||
```csharp
|
||||
PowerToysTelemetry.Log.WriteEvent(new MyModuleEvent("value", 42));
|
||||
```
|
||||
|
||||
**Privacy Tags (C#)**
|
||||
|
||||
| Tag | Use Case |
|
||||
| ------------- |:-------------:|
|
||||
| `PartA_PrivTags.ProductAndServiceUsage` [TelemetryBase.cs](../../src/common/Telemetry/TelemetryBase.cs) | Feature usage events
|
||||
| `PartA_PrivTags.ProductAndServicePerformance` [TelemetryBase.cs](../../src/common/Telemetry/TelemetryBase.cs) | Performance/timing events
|
||||
|
||||
### Update DATA_AND_PRIVACY.md file
|
||||
|
||||
Add your new event(s) to [DATA_AND_PRIVACY.md](../../DATA_AND_PRIVACY.md).
|
||||
|
||||
## Launch Product Version Containing the new events
|
||||
|
||||
Events do not become active until they ship in a released PowerToys version. After your PRs are merged:
|
||||
|
||||
- The event will begin firing once users install the version that includes it
|
||||
- In order for PowerToys to process these events, you must complete the next section
|
||||
|
||||
## Next Steps
|
||||
|
||||
Reach out to @carlos-zamora or @chatasweetie so internal scripts can process new event(s).
|
||||
|
||||
## Summary
|
||||
|
||||
Required steps:
|
||||
|
||||
1. In one PR:
|
||||
- Add the event(s) in code
|
||||
- Document event(s) in DATA_AND_PRIVACY.md
|
||||
1. Ship the change in a PowerToys release
|
||||
1. Reach out for next steps
|
||||
@@ -113,7 +113,7 @@ namespace Hosts.UITests
|
||||
|
||||
this.Find<NavigationViewItem>("Hosts File Editor").Click();
|
||||
|
||||
this.Find<ToggleSwitch>("Enable Hosts File Editor").Toggle(true);
|
||||
this.Find<ToggleSwitch>("Hosts File Editor").Toggle(true);
|
||||
this.Find<ToggleSwitch>("Launch as administrator").Toggle(launchAsAdmin);
|
||||
this.Find<ToggleSwitch>("Show a warning at startup").Toggle(showWarning);
|
||||
|
||||
|
||||
123
src/modules/LightSwitch/LightSwitchLib/LightSwitchLib.vcxproj
Normal file
123
src/modules/LightSwitch/LightSwitchLib/LightSwitchLib.vcxproj
Normal file
@@ -0,0 +1,123 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|ARM64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|ARM64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>17.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<ProjectGuid>{79267138-2895-4346-9021-21408d65379f}</ProjectGuid>
|
||||
<RootNamespace>LightSwitchLib</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0.26100.0</WindowsTargetPlatformVersion>
|
||||
<ProjectName>LightSwitchLib</ProjectName>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>StaticLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>StaticLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" Label="Configuration">
|
||||
<ConfigurationType>StaticLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="Configuration">
|
||||
<ConfigurationType>StaticLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\$(MSBuildProjectName)\</OutDir>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||
<AdditionalIncludeDirectories>
|
||||
./;
|
||||
..\..\..\common;
|
||||
..\..\..\common\logger;
|
||||
..\..\..\common\utils;
|
||||
..\..\..\..\deps\spdlog\include;
|
||||
%(AdditionalIncludeDirectories)
|
||||
</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="ThemeHelper.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ThemeHelper.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
|
||||
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<Import Project="..\..\..\..\deps\spdlog.props" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="Source Files">
|
||||
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
|
||||
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Header Files">
|
||||
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
|
||||
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Resource Files">
|
||||
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
|
||||
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="pch.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="ThemeHelper.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="pch.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ThemeHelper.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,9 +1,6 @@
|
||||
#include <windows.h>
|
||||
#include <logger/logger_settings.h>
|
||||
#include <logger/logger.h>
|
||||
#include <utils/logger_helper.h>
|
||||
#include "pch.h"
|
||||
#include "ThemeHelper.h"
|
||||
#include <SettingsConstants.h>
|
||||
#include <logger/logger.h>
|
||||
|
||||
// Controls changing the themes.
|
||||
|
||||
@@ -63,7 +60,7 @@ void SetSystemTheme(bool mode)
|
||||
if (mode) // if are changing to light mode
|
||||
{
|
||||
ResetColorPrevalence();
|
||||
Logger::info(L"[LightSwitchService] Reset ColorPrevalence to default when switching to light mode.");
|
||||
Logger::info(L"[LightSwitchLib] Reset ColorPrevalence to default when switching to light mode.");
|
||||
}
|
||||
|
||||
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
|
||||
@@ -136,4 +133,4 @@ bool IsNightLightEnabled()
|
||||
|
||||
RegCloseKey(hKey);
|
||||
return data[23] == 0x10 && data[24] == 0x00;
|
||||
}
|
||||
}
|
||||
10
src/modules/LightSwitch/LightSwitchLib/ThemeHelper.h
Normal file
10
src/modules/LightSwitch/LightSwitchLib/ThemeHelper.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
inline constexpr wchar_t PERSONALIZATION_REGISTRY_PATH[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
|
||||
inline constexpr wchar_t NIGHT_LIGHT_REGISTRY_PATH[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\CloudStore\\Store\\DefaultAccount\\Current\\default$windows.data.bluelightreduction.bluelightreductionstate\\windows.data.bluelightreduction.bluelightreductionstate";
|
||||
|
||||
void SetSystemTheme(bool isLight);
|
||||
void SetAppsTheme(bool isLight);
|
||||
bool GetCurrentSystemTheme();
|
||||
bool GetCurrentAppsTheme();
|
||||
bool IsNightLightEnabled();
|
||||
1
src/modules/LightSwitch/LightSwitchLib/pch.cpp
Normal file
1
src/modules/LightSwitch/LightSwitchLib/pch.cpp
Normal file
@@ -0,0 +1 @@
|
||||
#include "pch.h"
|
||||
5
src/modules/LightSwitch/LightSwitchLib/pch.h
Normal file
5
src/modules/LightSwitch/LightSwitchLib/pch.h
Normal file
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
|
||||
#include <windows.h>
|
||||
#include <vector>
|
||||
@@ -0,0 +1,22 @@
|
||||
#include "pch.h"
|
||||
#include "ThemeHelper.h"
|
||||
|
||||
extern "C" __declspec(dllexport) void __cdecl LightSwitch_SetSystemTheme(bool isLight)
|
||||
{
|
||||
SetSystemTheme(isLight);
|
||||
}
|
||||
|
||||
extern "C" __declspec(dllexport) void __cdecl LightSwitch_SetAppsTheme(bool isLight)
|
||||
{
|
||||
SetAppsTheme(isLight);
|
||||
}
|
||||
|
||||
extern "C" __declspec(dllexport) bool __cdecl LightSwitch_GetCurrentSystemTheme()
|
||||
{
|
||||
return GetCurrentSystemTheme();
|
||||
}
|
||||
|
||||
extern "C" __declspec(dllexport) bool __cdecl LightSwitch_GetCurrentAppsTheme()
|
||||
{
|
||||
return GetCurrentAppsTheme();
|
||||
}
|
||||
@@ -166,17 +166,17 @@
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<AdditionalIncludeDirectories>..\LightSwitchLib;..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
<ClInclude Include="ThemeHelper.h" />
|
||||
<ClInclude Include="trace.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="dllmain.cpp" />
|
||||
<ClCompile Include="ExportedFunctions.cpp" />
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
|
||||
@@ -187,7 +187,6 @@
|
||||
<PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">pch.h</PrecompiledHeaderFile>
|
||||
<PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">pch.h</PrecompiledHeaderFile>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ThemeHelper.cpp" />
|
||||
<ClCompile Include="trace.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
@@ -203,6 +202,9 @@
|
||||
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
|
||||
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\LightSwitchLib\LightSwitchLib.vcxproj">
|
||||
<Project>{79267138-2895-4346-9021-21408d65379f}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include <windows.h>
|
||||
#include "ThemeHelper.h"
|
||||
|
||||
// Controls changing the themes.
|
||||
static void ResetColorPrevalence()
|
||||
{
|
||||
HKEY hKey;
|
||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
0,
|
||||
KEY_SET_VALUE,
|
||||
&hKey) == ERROR_SUCCESS)
|
||||
{
|
||||
DWORD value = 0; // back to default value
|
||||
RegSetValueEx(hKey, L"ColorPrevalence", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
|
||||
RegCloseKey(hKey);
|
||||
|
||||
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
|
||||
|
||||
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
|
||||
|
||||
SendMessageTimeout(HWND_BROADCAST, WM_DWMCOLORIZATIONCOLORCHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void SetAppsTheme(bool mode)
|
||||
{
|
||||
HKEY hKey;
|
||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
0,
|
||||
KEY_SET_VALUE,
|
||||
&hKey) == ERROR_SUCCESS)
|
||||
{
|
||||
DWORD value = mode;
|
||||
RegSetValueEx(hKey, L"AppsUseLightTheme", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
|
||||
RegCloseKey(hKey);
|
||||
|
||||
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
|
||||
|
||||
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void SetSystemTheme(bool mode)
|
||||
{
|
||||
HKEY hKey;
|
||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
0,
|
||||
KEY_SET_VALUE,
|
||||
&hKey) == ERROR_SUCCESS)
|
||||
{
|
||||
DWORD value = mode;
|
||||
RegSetValueEx(hKey, L"SystemUsesLightTheme", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
|
||||
RegCloseKey(hKey);
|
||||
|
||||
if (mode) // if are changing to light mode
|
||||
{
|
||||
ResetColorPrevalence();
|
||||
}
|
||||
|
||||
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
|
||||
|
||||
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
bool GetCurrentSystemTheme()
|
||||
{
|
||||
HKEY hKey;
|
||||
DWORD value = 1; // default = light
|
||||
DWORD size = sizeof(value);
|
||||
|
||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
0,
|
||||
KEY_READ,
|
||||
&hKey) == ERROR_SUCCESS)
|
||||
{
|
||||
RegQueryValueEx(hKey, L"SystemUsesLightTheme", nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &size);
|
||||
RegCloseKey(hKey);
|
||||
}
|
||||
|
||||
return value == 1; // true = light, false = dark
|
||||
}
|
||||
|
||||
bool GetCurrentAppsTheme()
|
||||
{
|
||||
HKEY hKey;
|
||||
DWORD value = 1;
|
||||
DWORD size = sizeof(value);
|
||||
|
||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
0,
|
||||
KEY_READ,
|
||||
&hKey) == ERROR_SUCCESS)
|
||||
{
|
||||
RegQueryValueEx(hKey, L"AppsUseLightTheme", nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &size);
|
||||
RegCloseKey(hKey);
|
||||
}
|
||||
|
||||
return value == 1; // true = light, false = dark
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
#pragma once
|
||||
void SetSystemTheme(bool dark);
|
||||
void SetAppsTheme(bool dark);
|
||||
bool GetCurrentSystemTheme();
|
||||
bool GetCurrentAppsTheme();
|
||||
@@ -55,6 +55,7 @@
|
||||
<PreprocessorDefinitions>%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<AdditionalIncludeDirectories>
|
||||
./../;
|
||||
..\LightSwitchLib;
|
||||
..\..\..\common;
|
||||
..\..\..\common\logger;
|
||||
..\..\..\common\utils;
|
||||
@@ -78,7 +79,6 @@
|
||||
<ClCompile Include="LightSwitchStateManager.cpp" />
|
||||
<ClCompile Include="NightLightRegistryObserver.cpp" />
|
||||
<ClCompile Include="SettingsConstants.cpp" />
|
||||
<ClCompile Include="ThemeHelper.cpp" />
|
||||
<ClCompile Include="ThemeScheduler.cpp" />
|
||||
<ClCompile Include="trace.cpp" />
|
||||
<ClCompile Include="WinHookEventIDs.cpp" />
|
||||
@@ -93,7 +93,6 @@
|
||||
<ClInclude Include="NightLightRegistryObserver.h" />
|
||||
<ClInclude Include="SettingsConstants.h" />
|
||||
<ClInclude Include="SettingsObserver.h" />
|
||||
<ClInclude Include="ThemeHelper.h" />
|
||||
<ClInclude Include="ThemeScheduler.h" />
|
||||
<ClInclude Include="trace.h" />
|
||||
<ClInclude Include="WinHookEventIDs.h" />
|
||||
@@ -111,6 +110,9 @@
|
||||
<ProjectReference Include="..\..\..\common\Telemetry\EtwTrace\EtwTrace.vcxproj">
|
||||
<Project>{8f021b46-362b-485c-bfba-ccf83e820cbd}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\LightSwitchLib\LightSwitchLib.vcxproj">
|
||||
<Project>{79267138-2895-4346-9021-21408d65379f}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<Import Project="..\..\..\..\deps\spdlog.props" />
|
||||
@@ -118,4 +120,4 @@
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -12,6 +12,3 @@ enum class SettingId
|
||||
ChangeSystem,
|
||||
ChangeApps
|
||||
};
|
||||
|
||||
constexpr wchar_t PERSONALIZATION_REGISTRY_PATH[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
|
||||
constexpr wchar_t NIGHT_LIGHT_REGISTRY_PATH[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\CloudStore\\Store\\DefaultAccount\\Current\\default$windows.data.bluelightreduction.bluelightreductionstate\\windows.data.bluelightreduction.bluelightreductionstate";
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#pragma once
|
||||
void SetSystemTheme(bool dark);
|
||||
void SetAppsTheme(bool dark);
|
||||
bool GetCurrentSystemTheme();
|
||||
bool GetCurrentAppsTheme();
|
||||
bool IsNightLightEnabled();
|
||||
@@ -19,4 +19,9 @@
|
||||
<PackageReference Include="MSTest" />
|
||||
<ProjectReference Include="..\..\..\..\common\UITestAutomation\UITestAutomation.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="CopyNativeDll" AfterTargets="Build">
|
||||
<Copy SourceFiles="$(SolutionDir)$(Platform)\$(Configuration)\PowerToys.LightSwitchModuleInterface.dll" DestinationFolder="$(OutputPath)" SkipUnchangedFiles="true" />
|
||||
<Copy SourceFiles="$(SolutionDir)$(Platform)\$(Configuration)\LightSwitchLib\LightSwitchLib.lib" DestinationFolder="$(OutputPath)" SkipUnchangedFiles="true" Condition="Exists('$(SolutionDir)$(Platform)\$(Configuration)\LightSwitchLib\LightSwitchLib.lib')" ContinueOnError="true" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using Microsoft.PowerToys.UITest;
|
||||
@@ -17,6 +18,20 @@ namespace LightSwitch.UITests
|
||||
{
|
||||
private static readonly string[] ShortcutSeparators = { " + ", "+", " " };
|
||||
|
||||
[DllImport("PowerToys.LightSwitchModuleInterface.dll", CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern void LightSwitch_SetSystemTheme(bool isLight);
|
||||
|
||||
[DllImport("PowerToys.LightSwitchModuleInterface.dll", CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern void LightSwitch_SetAppsTheme(bool isLight);
|
||||
|
||||
[DllImport("PowerToys.LightSwitchModuleInterface.dll", CallingConvention = CallingConvention.Cdecl)]
|
||||
[return: MarshalAs(UnmanagedType.I1)]
|
||||
private static extern bool LightSwitch_GetCurrentSystemTheme();
|
||||
|
||||
[DllImport("PowerToys.LightSwitchModuleInterface.dll", CallingConvention = CallingConvention.Cdecl)]
|
||||
[return: MarshalAs(UnmanagedType.I1)]
|
||||
private static extern bool LightSwitch_GetCurrentAppsTheme();
|
||||
|
||||
/// <summary>
|
||||
/// Performs common test initialization: navigate to settings, enable toggle, verify shortcut
|
||||
/// </summary>
|
||||
@@ -127,8 +142,7 @@ namespace LightSwitch.UITests
|
||||
/// <param name="testBase">The test base instance</param>
|
||||
public static void CleanupTest(UITestBase testBase)
|
||||
{
|
||||
// TODO: Make sure the task kills?
|
||||
// CloseLightSwitch(testBase);
|
||||
CloseLightSwitch(testBase);
|
||||
|
||||
// Ensure we're attached to settings after cleanup
|
||||
try
|
||||
@@ -141,6 +155,51 @@ namespace LightSwitch.UITests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switch to white/light theme for both system and apps
|
||||
/// </summary>
|
||||
/// <param name="testBase">The test base instance</param>
|
||||
public static void CloseLightSwitch(UITestBase testBase)
|
||||
{
|
||||
// Kill LightSwitch process before setting themes
|
||||
KillLightSwitchProcess();
|
||||
|
||||
// Set both themes to light (white)
|
||||
SetSystemTheme(true);
|
||||
SetAppsTheme(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kill the LightSwitch service process if it's running
|
||||
/// </summary>
|
||||
private static void KillLightSwitchProcess()
|
||||
{
|
||||
try
|
||||
{
|
||||
var processes = System.Diagnostics.Process.GetProcessesByName("PowerToys.LightSwitchService");
|
||||
foreach (var process in processes)
|
||||
{
|
||||
try
|
||||
{
|
||||
process.Kill();
|
||||
process.WaitForExit(2000);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors killing individual processes
|
||||
}
|
||||
finally
|
||||
{
|
||||
process.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors enumerating processes
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a update time test operation
|
||||
/// </summary>
|
||||
@@ -408,24 +467,22 @@ namespace LightSwitch.UITests
|
||||
/* Helpers */
|
||||
private static int GetSystemTheme()
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
|
||||
if (key is null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
return (int)key.GetValue("SystemUsesLightTheme", 1);
|
||||
return LightSwitch_GetCurrentSystemTheme() ? 1 : 0;
|
||||
}
|
||||
|
||||
private static int GetAppsTheme()
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
|
||||
if (key is null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
return LightSwitch_GetCurrentAppsTheme() ? 1 : 0;
|
||||
}
|
||||
|
||||
return (int)key.GetValue("AppsUseLightTheme", 1);
|
||||
private static void SetSystemTheme(bool isLight)
|
||||
{
|
||||
LightSwitch_SetSystemTheme(isLight);
|
||||
}
|
||||
|
||||
private static void SetAppsTheme(bool isLight)
|
||||
{
|
||||
LightSwitch_SetAppsTheme(isLight);
|
||||
}
|
||||
|
||||
private static string GetHelpTextValue(string helpText, string key)
|
||||
|
||||
@@ -456,10 +456,11 @@ namespace MouseUtils.UITests
|
||||
var groupAppearanceBehavior = foundCustom.Find(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseAppearanceBehavior));
|
||||
if (groupAppearanceBehavior != null)
|
||||
{
|
||||
// groupAppearanceBehavior.Click();
|
||||
if (foundCustom.FindAll(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseOverlayOpacity)).Count == 0)
|
||||
var expandState = groupAppearanceBehavior.Selected;
|
||||
if (!expandState)
|
||||
{
|
||||
groupAppearanceBehavior.Click();
|
||||
Task.Delay(500).Wait();
|
||||
}
|
||||
|
||||
// Set the BackGround color
|
||||
@@ -541,15 +542,6 @@ namespace MouseUtils.UITests
|
||||
Task.Delay(500).Wait();
|
||||
spotlightColorButton.Click(false, 500, 1500);
|
||||
|
||||
// Set the overlay opacity to overlayOpacity%
|
||||
var overlayOpacitySlider = foundCustom.Find<Slider>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseOverlayOpacity));
|
||||
Assert.IsNotNull(overlayOpacitySlider);
|
||||
Assert.IsNotNull(settings.OverlayOpacity);
|
||||
int overlayOpacityValue = int.Parse(settings.OverlayOpacity, CultureInfo.InvariantCulture);
|
||||
overlayOpacitySlider.QuickSetValue(overlayOpacityValue);
|
||||
Assert.AreEqual(settings.OverlayOpacity, overlayOpacitySlider.Text);
|
||||
Task.Delay(1000).Wait();
|
||||
|
||||
// Set the Fade Initial zoom to 0
|
||||
var spotlightInitialZoomSlider = foundCustom.Find<Slider>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseSpotlightZoom));
|
||||
Assert.IsNotNull(spotlightInitialZoomSlider);
|
||||
@@ -592,7 +584,7 @@ namespace MouseUtils.UITests
|
||||
// Assert.IsNull(animationDisabledWarning);
|
||||
if (foundElements.Count != 0)
|
||||
{
|
||||
var openSettingsLink = foundCustom.Find<Element>("Open settings");
|
||||
var openSettingsLink = foundCustom.Find<Element>("Open animation settings");
|
||||
Assert.IsNotNull(openSettingsLink);
|
||||
openSettingsLink.Click(false, 500, 3000);
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ namespace MouseUtils.UITests
|
||||
public const string FindMyMouseExcludedApps = "MouseUtils_FindMyMouseExcludedAppsId";
|
||||
public const string FindMyMouseBackgroundColor = "MouseUtils_FindMyMouseBackgroundColorId";
|
||||
public const string FindMyMouseSpotlightColor = "MouseUtils_FindMyMouseSpotlightColorId";
|
||||
public const string FindMyMouseOverlayOpacity = "MouseUtils_FindMyMouseOverlayOpacityId";
|
||||
public const string FindMyMouseSpotlightZoom = "MouseUtils_FindMyMouseSpotlightZoomId";
|
||||
public const string FindMyMouseSpotlightRadius = "MouseUtils_FindMyMouseSpotlightRadiusId";
|
||||
public const string FindMyMouseAnimationDuration = "MouseUtils_FindMyMouseAnimationDurationId";
|
||||
@@ -72,10 +71,10 @@ namespace MouseUtils.UITests
|
||||
|
||||
private static readonly Dictionary<MouseUtils, string> MouseUtilUIToggleMap = new()
|
||||
{
|
||||
[MouseUtils.MouseHighlighter] = @"Enable Mouse Highlighter",
|
||||
[MouseUtils.FindMyMouse] = @"Enable Find My Mouse",
|
||||
[MouseUtils.MousePointerCrosshairs] = @"Enable Mouse Pointer Crosshairs",
|
||||
[MouseUtils.MouseJump] = @"Enable Mouse Jump",
|
||||
[MouseUtils.MouseHighlighter] = @"Mouse Highlighter",
|
||||
[MouseUtils.FindMyMouse] = @"Find My Mouse",
|
||||
[MouseUtils.MousePointerCrosshairs] = @"Mouse Pointer Crosshairs",
|
||||
[MouseUtils.MouseJump] = @"Mouse Jump",
|
||||
};
|
||||
|
||||
public static string GetMouseUtilUIName(MouseUtils element)
|
||||
|
||||
@@ -57,7 +57,7 @@ public class WorkspacesSettingsTests : UITestBase
|
||||
GoToSettingsPageAndEnable();
|
||||
|
||||
// Find the enable toggle
|
||||
var enableToggle = Find<ToggleSwitch>("Enable Workspaces");
|
||||
var enableToggle = Find<ToggleSwitch>("Workspaces");
|
||||
Assert.IsNotNull(enableToggle, "Enable Workspaces toggle should exist");
|
||||
|
||||
Assert.IsTrue(enableToggle.IsOn, "Enable Workspaces toggle should be in the 'on' state");
|
||||
@@ -80,7 +80,7 @@ public class WorkspacesSettingsTests : UITestBase
|
||||
public void TestLaunchEditorByActivationShortcut()
|
||||
{
|
||||
// Ensure module is enabled
|
||||
var enableToggle = Find<ToggleSwitch>("Enable Workspaces");
|
||||
var enableToggle = Find<ToggleSwitch>("Workspaces");
|
||||
if (!enableToggle.IsOn)
|
||||
{
|
||||
enableToggle.Click();
|
||||
@@ -109,7 +109,7 @@ public class WorkspacesSettingsTests : UITestBase
|
||||
public void TestDisabledModuleDoesNotLaunchByShortcut()
|
||||
{
|
||||
// Disable the module
|
||||
var enableToggle = Find<ToggleSwitch>("Enable Workspaces");
|
||||
var enableToggle = Find<ToggleSwitch>("Workspaces");
|
||||
if (enableToggle.IsOn)
|
||||
{
|
||||
enableToggle.Click();
|
||||
@@ -131,7 +131,7 @@ public class WorkspacesSettingsTests : UITestBase
|
||||
RestartScopeExe();
|
||||
NavigateToWorkspacesSettings();
|
||||
|
||||
enableToggle = Find<ToggleSwitch>("Enable Workspaces");
|
||||
enableToggle = Find<ToggleSwitch>("Workspaces");
|
||||
if (!enableToggle.IsOn)
|
||||
{
|
||||
enableToggle.Click();
|
||||
@@ -174,7 +174,7 @@ public class WorkspacesSettingsTests : UITestBase
|
||||
|
||||
this.Find<NavigationViewItem>("Workspaces").Click();
|
||||
|
||||
var enableButton = this.Find<ToggleSwitch>("Enable Workspaces");
|
||||
var enableButton = this.Find<ToggleSwitch>("Workspaces");
|
||||
Assert.IsNotNull(enableButton, "Enable Workspaces toggle should exist");
|
||||
|
||||
if (!enableButton.IsOn)
|
||||
|
||||
@@ -26,11 +26,6 @@
|
||||
"input": "pushd .\\ExtensionTemplate\\ ; git archive -o ..\\Microsoft.CmdPal.UI.ViewModels\\Assets\\template.zip HEAD -- .\\TemplateCmdPalExtension\\ ; popd",
|
||||
"name": "Update template project",
|
||||
"description": "zips up the ExtensionTemplate into our assets. Run this in the cmdpal/ directory."
|
||||
},
|
||||
{
|
||||
"input": " .\\extensionsdk\\nuget\\BuildSDKHelper.ps1 -VersionOfSDK 0.0.1",
|
||||
"name": "Build SDK",
|
||||
"description": "Builds the SDK nuget package with the specified version."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common;
|
||||
|
||||
public static class CoreLogger
|
||||
@@ -13,8 +15,6 @@ public static class CoreLogger
|
||||
|
||||
private static ILogger? _logger;
|
||||
|
||||
public static ILogger? Instance => _logger;
|
||||
|
||||
public static void LogError(string message, Exception ex, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
|
||||
{
|
||||
_logger?.LogError(message, ex, memberName, sourceFilePath, sourceLineNumber);
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Helpers;
|
||||
|
||||
public partial class PinnedDockItem : WrappedDockItem
|
||||
{
|
||||
public override string Title => $"{base.Title} ({Properties.Resources.PinnedItemSuffix})";
|
||||
|
||||
public PinnedDockItem(ICommand command)
|
||||
: base(command, command.Name)
|
||||
{
|
||||
}
|
||||
|
||||
public PinnedDockItem(IListItem item, string id)
|
||||
: base([item], id, item.Title)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -9,19 +9,4 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Properties {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public class Resources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Core.Common.Properties.Resources", typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Pinned.
|
||||
/// </summary>
|
||||
public static string PinnedItemSuffix {
|
||||
get {
|
||||
return ResourceManager.GetString("PinnedItemSuffix", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="PinnedItemSuffix" xml:space="preserve">
|
||||
<value>Pinned</value>
|
||||
<comment>Suffix shown for pinned items in the dock</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -96,14 +96,9 @@ public partial class CommandBarViewModel : ObservableObject,
|
||||
|
||||
SecondaryCommand = SelectedItem.SecondaryCommand;
|
||||
|
||||
var hasMoreThanOneContextItem = SelectedItem.MoreCommands.Count() > 1;
|
||||
var hasMoreThanOneCommand = SelectedItem.MoreCommands.OfType<CommandContextItemViewModel>().Any();
|
||||
|
||||
// ShouldShowContextMenu = SelectedItem.MoreCommands
|
||||
|
||||
// // .OfType<CommandContextItemViewModel>()
|
||||
// .Count() > 1;
|
||||
ShouldShowContextMenu = hasMoreThanOneContextItem && hasMoreThanOneCommand;
|
||||
ShouldShowContextMenu = SelectedItem.MoreCommands
|
||||
.OfType<CommandContextItemViewModel>()
|
||||
.Count() > 1;
|
||||
|
||||
OnPropertyChanged(nameof(HasSecondaryCommand));
|
||||
OnPropertyChanged(nameof(SecondaryCommand));
|
||||
|
||||
@@ -42,9 +42,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
private string _itemTitle = string.Empty;
|
||||
|
||||
protected string ItemTitle => _itemTitle;
|
||||
|
||||
public virtual string Title => string.IsNullOrEmpty(_itemTitle) ? Name : _itemTitle;
|
||||
public string Title => string.IsNullOrEmpty(_itemTitle) ? Name : _itemTitle;
|
||||
|
||||
public string Subtitle { get; private set; } = string.Empty;
|
||||
|
||||
@@ -66,30 +64,10 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
public CommandItemViewModel? PrimaryCommand => this;
|
||||
|
||||
public CommandItemViewModel? SecondaryCommand // => HasMoreCommands ? ActualCommands[0] : null;
|
||||
{
|
||||
get
|
||||
{
|
||||
if (HasMoreCommands)
|
||||
{
|
||||
if (MoreCommands[0] is CommandContextItemViewModel command)
|
||||
{
|
||||
return command;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public CommandItemViewModel? SecondaryCommand => HasMoreCommands ? ActualCommands[0] : null;
|
||||
|
||||
public bool ShouldBeVisible => !string.IsNullOrEmpty(Name);
|
||||
|
||||
public bool HasTitle => !string.IsNullOrEmpty(Title);
|
||||
|
||||
public bool HasSubtitle => !string.IsNullOrEmpty(Subtitle);
|
||||
|
||||
public virtual bool HasText => HasTitle || HasSubtitle;
|
||||
|
||||
public DataPackageView? DataPackage { get; private set; }
|
||||
|
||||
public List<IContextItemViewModel> AllCommands
|
||||
@@ -353,19 +331,16 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
UpdateProperty(nameof(Name));
|
||||
UpdateProperty(nameof(Title));
|
||||
UpdateProperty(nameof(Icon));
|
||||
UpdateProperty(nameof(HasText));
|
||||
break;
|
||||
|
||||
case nameof(Title):
|
||||
_itemTitle = model.Title;
|
||||
UpdateProperty(nameof(HasText));
|
||||
break;
|
||||
|
||||
case nameof(Subtitle):
|
||||
var modelSubtitle = model.Subtitle;
|
||||
this.Subtitle = modelSubtitle;
|
||||
_defaultCommandContextItemViewModel?.Subtitle = modelSubtitle;
|
||||
UpdateProperty(nameof(HasText));
|
||||
break;
|
||||
|
||||
case nameof(Icon):
|
||||
@@ -452,10 +427,11 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateDefaultContextItemIcon() =>
|
||||
|
||||
private void UpdateDefaultContextItemIcon()
|
||||
{
|
||||
// Command icon takes precedence over our icon on the primary command
|
||||
_defaultCommandContextItemViewModel?.UpdateIcon(Command.Icon.IsSet ? Command.Icon : _icon);
|
||||
}
|
||||
|
||||
private void UpdateTitle(string? title)
|
||||
{
|
||||
|
||||
@@ -53,12 +53,11 @@ public partial class ContextMenuViewModel : ObservableObject,
|
||||
{
|
||||
if (SelectedItem is not null)
|
||||
{
|
||||
// if (SelectedItem.MoreCommands.Count() > 1)
|
||||
// {
|
||||
ContextMenuStack.Clear();
|
||||
PushContextStack(SelectedItem.AllCommands);
|
||||
|
||||
// }
|
||||
if (SelectedItem.MoreCommands.Count() > 1)
|
||||
{
|
||||
ContextMenuStack.Clear();
|
||||
PushContextStack(SelectedItem.AllCommands);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ public abstract partial class ExtensionObjectViewModel : ObservableObject
|
||||
PageContext = new(realContext);
|
||||
}
|
||||
|
||||
protected ExtensionObjectViewModel(WeakReference<IPageContext> context)
|
||||
internal ExtensionObjectViewModel(WeakReference<IPageContext> context)
|
||||
{
|
||||
PageContext = context;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ public partial class LoadingPageViewModel : PageViewModel
|
||||
: base(model, scheduler, host)
|
||||
{
|
||||
ModelIsLoading = true;
|
||||
HasBackButton = false;
|
||||
IsInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
|
||||
public record NavigateToPageMessage(PageViewModel Page, bool WithAnimation, CancellationToken CancellationToken, bool TransientPage = false);
|
||||
public record NavigateToPageMessage(PageViewModel Page, bool WithAnimation, CancellationToken CancellationToken);
|
||||
|
||||
@@ -18,8 +18,6 @@ public record PerformCommandMessage
|
||||
|
||||
public bool WithAnimation { get; set; } = true;
|
||||
|
||||
public bool TransientPage { get; set; }
|
||||
|
||||
public PerformCommandMessage(ExtensionObject<ICommand> command)
|
||||
{
|
||||
Command = command;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
|
||||
public sealed record WindowHiddenMessage();
|
||||
@@ -27,10 +27,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
||||
public partial string ErrorMessage { get; protected set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsRootPage { get; set; } = true;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool HasBackButton { get; set; } = true;
|
||||
public partial bool IsNested { get; set; } = true;
|
||||
|
||||
// This is set from the SearchBar
|
||||
[ObservableProperty]
|
||||
|
||||
@@ -121,8 +121,4 @@
|
||||
<value>Show details</value>
|
||||
<comment>Name for the command that shows details of an item</comment>
|
||||
</data>
|
||||
<data name="PinnedItemSuffix" xml:space="preserve">
|
||||
<value>Pinned</value>
|
||||
<comment>Suffix shown for pinned items in the dock</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -16,8 +16,7 @@ namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
public partial class ShellViewModel : ObservableObject,
|
||||
IDisposable,
|
||||
IRecipient<PerformCommandMessage>,
|
||||
IRecipient<HandleCommandResultMessage>,
|
||||
IRecipient<WindowHiddenMessage>
|
||||
IRecipient<HandleCommandResultMessage>
|
||||
{
|
||||
private readonly IRootPageService _rootPageService;
|
||||
private readonly IAppHostService _appHostService;
|
||||
@@ -80,9 +79,8 @@ public partial class ShellViewModel : ObservableObject,
|
||||
private IPage? _rootPage;
|
||||
|
||||
private bool _isNested;
|
||||
private bool _currentlyTransient;
|
||||
|
||||
public bool IsNested => _isNested && !_currentlyTransient;
|
||||
public bool IsNested => _isNested;
|
||||
|
||||
public PageViewModel NullPage { get; private set; }
|
||||
|
||||
@@ -98,13 +96,11 @@ public partial class ShellViewModel : ObservableObject,
|
||||
_appHostService = appHostService;
|
||||
|
||||
NullPage = new NullPageViewModel(_scheduler, appHostService.GetDefaultHost());
|
||||
NullPage.HasBackButton = false;
|
||||
_currentPage = new LoadingPageViewModel(null, _scheduler, appHostService.GetDefaultHost());
|
||||
|
||||
// Register to receive messages
|
||||
WeakReferenceMessenger.Default.Register<PerformCommandMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<HandleCommandResultMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<WindowHiddenMessage>(this);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -263,7 +259,7 @@ public partial class ShellViewModel : ObservableObject,
|
||||
|
||||
var host = _appHostService.GetHostForCommand(message.Context, CurrentPage.ExtensionHost);
|
||||
|
||||
_rootPageService.OnPerformCommand(message.Context, CurrentPage.IsRootPage, host);
|
||||
_rootPageService.OnPerformCommand(message.Context, !CurrentPage.IsNested, host);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -273,7 +269,6 @@ public partial class ShellViewModel : ObservableObject,
|
||||
|
||||
var isMainPage = command == _rootPage;
|
||||
_isNested = !isMainPage;
|
||||
_currentlyTransient = message.TransientPage;
|
||||
|
||||
// Telemetry: Track extension page navigation for session metrics
|
||||
if (host is not null)
|
||||
@@ -293,9 +288,6 @@ public partial class ShellViewModel : ObservableObject,
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
pageViewModel.IsRootPage = isMainPage;
|
||||
pageViewModel.HasBackButton = IsNested;
|
||||
|
||||
// Clear command bar, ViewModel initialization can already set new commands if it wants to
|
||||
OnUIThread(() => WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null)));
|
||||
|
||||
@@ -315,8 +307,7 @@ public partial class ShellViewModel : ObservableObject,
|
||||
_scheduler);
|
||||
|
||||
// While we're loading in the background, immediately move to the next page.
|
||||
NavigateToPageMessage msg = new(pageViewModel, message.WithAnimation, navigationToken, message.TransientPage);
|
||||
WeakReferenceMessenger.Default.Send(msg);
|
||||
WeakReferenceMessenger.Default.Send<NavigateToPageMessage>(new(pageViewModel, message.WithAnimation, navigationToken));
|
||||
|
||||
// Note: Originally we set our page back in the ViewModel here, but that now happens in response to the Frame navigating triggered from the above
|
||||
// See RootFrame_Navigated event handler.
|
||||
@@ -487,19 +478,6 @@ public partial class ShellViewModel : ObservableObject,
|
||||
UnsafeHandleCommandResult(message.Result.Unsafe);
|
||||
}
|
||||
|
||||
public void Receive(WindowHiddenMessage message)
|
||||
{
|
||||
// If the window was hidden while we had a transient page, we need to reset that state.
|
||||
if (_currentlyTransient)
|
||||
{
|
||||
_currentlyTransient = false;
|
||||
|
||||
// navigate back to the main page without animation
|
||||
GoHome(withAnimation: false, focusSearch: false);
|
||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(new ExtensionObject<ICommand>(_rootPage)));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUIThread(Action action)
|
||||
{
|
||||
_ = Task.Factory.StartNew(
|
||||
|
||||
@@ -21,7 +21,7 @@ public class CommandPalettePageViewModelFactory
|
||||
{
|
||||
return page switch
|
||||
{
|
||||
IListPage listPage => new ListViewModel(listPage, _scheduler, host) { IsRootPage = !nested },
|
||||
IListPage listPage => new ListViewModel(listPage, _scheduler, host) { IsNested = nested },
|
||||
IContentPage contentPage => new CommandPaletteContentPageViewModel(contentPage, _scheduler, host),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.Common;
|
||||
using Microsoft.CmdPal.Core.Common.Services;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using Windows.Foundation;
|
||||
@@ -29,8 +27,6 @@ public sealed class CommandProviderWrapper
|
||||
|
||||
public TopLevelViewModel[] FallbackItems { get; private set; } = [];
|
||||
|
||||
public TopLevelViewModel[] DockBandItems { get; private set; } = [];
|
||||
|
||||
public string DisplayName { get; private set; } = string.Empty;
|
||||
|
||||
public IExtensionWrapper? Extension { get; }
|
||||
@@ -145,7 +141,6 @@ public sealed class CommandProviderWrapper
|
||||
|
||||
ICommandItem[]? commands = null;
|
||||
IFallbackCommandItem[]? fallbacks = null;
|
||||
ICommandItem[] dockBands = []; // do not initialize me to null
|
||||
|
||||
try
|
||||
{
|
||||
@@ -163,30 +158,6 @@ public sealed class CommandProviderWrapper
|
||||
UnsafePreCacheApiAdditions(two);
|
||||
}
|
||||
|
||||
// if (model is IExtendedAttributesProvider iHaveProperties)
|
||||
if (model is ICommandProvider3 supportsDockBands)
|
||||
{
|
||||
// var props = iHaveProperties.GetProperties();
|
||||
// var hasBands = props.TryGetValue("DockBands", out var obj);
|
||||
// if (hasBands && obj is not null)
|
||||
// {
|
||||
// // CoreLogger.LogDebug($"Found bands object on {DisplayName} ({ProviderId}) ");
|
||||
// // var bands = (ICommandItem[])obj;
|
||||
// var bands = obj as ICommandItem[];
|
||||
// if (bands is not null)
|
||||
// {
|
||||
// CoreLogger.LogDebug($"Found {bands.Length} bands on {DisplayName} ({ProviderId}) ");
|
||||
// dockBands = bands;
|
||||
// }
|
||||
// }
|
||||
var bands = supportsDockBands.GetDockBands();
|
||||
if (bands is not null)
|
||||
{
|
||||
CoreLogger.LogDebug($"Found {bands.Length} bands on {DisplayName} ({ProviderId}) ");
|
||||
dockBands = bands;
|
||||
}
|
||||
}
|
||||
|
||||
Id = model.Id;
|
||||
DisplayName = model.DisplayName;
|
||||
Icon = new(model.Icon);
|
||||
@@ -197,8 +168,7 @@ public sealed class CommandProviderWrapper
|
||||
Settings = new(model.Settings, this, _taskScheduler);
|
||||
|
||||
// We do need to explicitly initialize commands though
|
||||
var objects = new TopLevelObjects(commands, fallbacks, dockBands);
|
||||
InitializeCommands(objects, serviceProvider, pageContext);
|
||||
InitializeCommands(commands, fallbacks, serviceProvider, pageContext);
|
||||
|
||||
Logger.LogDebug($"Loaded commands from {DisplayName} ({ProviderId})");
|
||||
}
|
||||
@@ -210,68 +180,33 @@ public sealed class CommandProviderWrapper
|
||||
}
|
||||
}
|
||||
|
||||
private record TopLevelObjects(
|
||||
ICommandItem[]? Commands,
|
||||
IFallbackCommandItem[]? Fallbacks,
|
||||
ICommandItem[]? DockBands);
|
||||
|
||||
private void InitializeCommands(
|
||||
TopLevelObjects objects,
|
||||
IServiceProvider serviceProvider,
|
||||
WeakReference<IPageContext> pageContext)
|
||||
private void InitializeCommands(ICommandItem[] commands, IFallbackCommandItem[] fallbacks, IServiceProvider serviceProvider, WeakReference<IPageContext> pageContext)
|
||||
{
|
||||
var settings = serviceProvider.GetService<SettingsModel>()!;
|
||||
var state = serviceProvider.GetService<AppStateModel>()!;
|
||||
var providerSettings = GetProviderSettings(settings);
|
||||
|
||||
Func<ICommandItem?, TopLevelType, TopLevelViewModel> make = (ICommandItem? i, TopLevelType t) =>
|
||||
Func<ICommandItem?, bool, TopLevelViewModel> makeAndAdd = (ICommandItem? i, bool fallback) =>
|
||||
{
|
||||
CommandItemViewModel commandItemViewModel = new(new(i), pageContext);
|
||||
TopLevelViewModel topLevelViewModel = new(commandItemViewModel, t, ExtensionHost, ProviderId, settings, providerSettings, serviceProvider, i);
|
||||
TopLevelViewModel topLevelViewModel = new(commandItemViewModel, fallback, ExtensionHost, ProviderId, settings, providerSettings, serviceProvider, i);
|
||||
topLevelViewModel.InitializeProperties();
|
||||
|
||||
return topLevelViewModel;
|
||||
};
|
||||
|
||||
if (objects.Commands is not null)
|
||||
if (commands is not null)
|
||||
{
|
||||
TopLevelItems = objects.Commands
|
||||
.Select(c => make(c, TopLevelType.Normal))
|
||||
TopLevelItems = commands
|
||||
.Select(c => makeAndAdd(c, false))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
if (objects.Fallbacks is not null)
|
||||
if (fallbacks is not null)
|
||||
{
|
||||
FallbackItems = objects.Fallbacks
|
||||
.Select(c => make(c, TopLevelType.Fallback))
|
||||
FallbackItems = fallbacks
|
||||
.Select(c => makeAndAdd(c, true))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
if (objects.DockBands is not null)
|
||||
{
|
||||
List<TopLevelViewModel> bands = new();
|
||||
foreach (var b in objects.DockBands)
|
||||
{
|
||||
var bandVm = make(b, TopLevelType.DockBand);
|
||||
bands.Add(bandVm);
|
||||
}
|
||||
|
||||
foreach (var c in TopLevelItems)
|
||||
{
|
||||
foreach (var pinnedId in settings.DockSettings.PinnedCommands)
|
||||
{
|
||||
if (pinnedId == c.Id)
|
||||
{
|
||||
var bandModel = c.ToPinnedDockBandItem();
|
||||
var bandVm = make(bandModel, TopLevelType.DockBand);
|
||||
bands.Add(bandVm);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DockBandItems = bands.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private void UnsafePreCacheApiAdditions(ICommandProvider2 provider)
|
||||
@@ -284,10 +219,6 @@ public sealed class CommandProviderWrapper
|
||||
{
|
||||
Logger.LogDebug($"{ProviderId}: Found an IExtendedAttributesProvider");
|
||||
}
|
||||
else if (a is ICommandItem[] commands)
|
||||
{
|
||||
Logger.LogDebug($"{ProviderId}: Found an ICommandItem[]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,17 +235,4 @@ public sealed class CommandProviderWrapper
|
||||
// In handling this, a call will be made to `LoadTopLevelCommands` to
|
||||
// retrieve the new items.
|
||||
this.CommandsChanged?.Invoke(this, args);
|
||||
|
||||
internal void PinDockBand(TopLevelViewModel bandVm)
|
||||
{
|
||||
Logger.LogDebug($"CommandProviderWrapper.PinDockBand: {ProviderId} - {bandVm.Id}");
|
||||
|
||||
// var settings = ExtensionHost.ServiceProvider.GetService<SettingsModel>()!;
|
||||
// settings.DockSettings.PinnedCommands.Add(bandVm.Id);
|
||||
// SettingsModel.SaveSettings(settings);
|
||||
var bands = this.DockBandItems.ToList();
|
||||
bands.Add(bandVm);
|
||||
this.DockBandItems = bands.ToArray();
|
||||
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Foundation.Collections;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
|
||||
|
||||
@@ -21,8 +18,6 @@ public sealed partial class BuiltInsCommandProvider : CommandProvider
|
||||
private readonly FallbackLogItem _fallbackLogItem = new();
|
||||
private readonly NewExtensionPage _newExtension = new();
|
||||
|
||||
private readonly IRootPageService _rootPageService;
|
||||
|
||||
public override ICommandItem[] TopLevelCommands() =>
|
||||
[
|
||||
new CommandItem(openSettings) { },
|
||||
@@ -42,22 +37,11 @@ public sealed partial class BuiltInsCommandProvider : CommandProvider
|
||||
_fallbackLogItem,
|
||||
];
|
||||
|
||||
public BuiltInsCommandProvider(IRootPageService rootPageService)
|
||||
public BuiltInsCommandProvider()
|
||||
{
|
||||
Id = "com.microsoft.cmdpal.builtin.core";
|
||||
DisplayName = Properties.Resources.builtin_display_name;
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\Square44x44Logo.altform-unplated_targetsize-256.png");
|
||||
|
||||
_rootPageService = rootPageService;
|
||||
}
|
||||
|
||||
public override ICommandItem[]? GetDockBands()
|
||||
{
|
||||
var rootPage = _rootPageService.GetRootPage();
|
||||
List<ICommandItem> bandItems = new();
|
||||
bandItems.Add(new WrappedDockItem(rootPage, Properties.Resources.builtin_command_palette_title));
|
||||
|
||||
return bandItems.ToArray();
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png");
|
||||
}
|
||||
|
||||
public override void InitializeWithHost(IExtensionHost host) => BuiltinsExtensionHost.Instance.Initialize(host);
|
||||
|
||||
@@ -50,7 +50,6 @@ public partial class MainListPage : DynamicListPage,
|
||||
|
||||
public MainListPage(TopLevelCommandManager topLevelCommandManager, SettingsModel settings, AliasManager aliasManager, AppStateModel appStateModel)
|
||||
{
|
||||
Id = "com.microsoft.cmdpal.home";
|
||||
Title = Resources.builtin_home_name;
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png");
|
||||
PlaceholderText = Properties.Resources.builtin_main_list_page_searchbar_placeholder;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -19,7 +19,7 @@ public partial class OpenSettingsCommand : InvokableCommand
|
||||
|
||||
public override ICommandResult Invoke()
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>(new());
|
||||
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>();
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -70,15 +70,6 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
|
||||
StateJson = model.StateJson;
|
||||
DataJson = model.DataJson;
|
||||
|
||||
RenderCard();
|
||||
|
||||
UpdateProperty(nameof(Card));
|
||||
|
||||
model.PropChanged += Model_PropChanged;
|
||||
}
|
||||
|
||||
private void RenderCard()
|
||||
{
|
||||
if (TryBuildCard(TemplateJson, DataJson, out var builtCard, out var renderingError))
|
||||
{
|
||||
Card = builtCard;
|
||||
@@ -102,41 +93,8 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
|
||||
UpdateProperty(nameof(Card));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void Model_PropChanged(object sender, IPropChangedEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
FetchProperty(args.PropertyName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void FetchProperty(string propertyName)
|
||||
{
|
||||
var model = this._formModel.Unsafe;
|
||||
if (model is null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
|
||||
switch (propertyName)
|
||||
{
|
||||
case nameof(DataJson):
|
||||
DataJson = model.DataJson;
|
||||
RenderCard();
|
||||
break;
|
||||
case nameof(TemplateJson):
|
||||
TemplateJson = model.TemplateJson;
|
||||
RenderCard();
|
||||
break;
|
||||
}
|
||||
|
||||
UpdateProperty(propertyName);
|
||||
UpdateProperty(nameof(Card));
|
||||
}
|
||||
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(AdaptiveOpenUrlAction))]
|
||||
|
||||
@@ -1,216 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
#pragma warning disable SA1402 // File may only contain a single type
|
||||
|
||||
public partial class DockBandSettingsViewModel : ObservableObject
|
||||
{
|
||||
private static readonly CompositeFormat PluralItemsFormatString = CompositeFormat.Parse(Properties.Resources.dock_item_count_plural);
|
||||
private readonly SettingsModel _settingsModel;
|
||||
private readonly DockBandSettings _dockSettingsModel;
|
||||
private readonly TopLevelViewModel _adapter;
|
||||
private readonly DockBandViewModel? _bandViewModel;
|
||||
|
||||
public string Title => _adapter.Title;
|
||||
|
||||
public string Description
|
||||
{
|
||||
get
|
||||
{
|
||||
List<string> parts = [_adapter.ExtensionName];
|
||||
|
||||
// Add the number of items in the band
|
||||
var itemCount = NumItemsInBand();
|
||||
if (itemCount > 0)
|
||||
{
|
||||
var itemsString = itemCount == 1 ?
|
||||
Properties.Resources.dock_item_count_singular :
|
||||
string.Format(CultureInfo.CurrentCulture, PluralItemsFormatString, itemCount);
|
||||
parts.Add(itemsString);
|
||||
}
|
||||
|
||||
return string.Join(" - ", parts);
|
||||
}
|
||||
}
|
||||
|
||||
public string ProviderId => _adapter.CommandProviderId;
|
||||
|
||||
public IconInfoViewModel Icon => _adapter.IconViewModel;
|
||||
|
||||
private ShowLabelsOption _showLabels;
|
||||
|
||||
public ShowLabelsOption ShowLabels
|
||||
{
|
||||
get => _showLabels;
|
||||
set
|
||||
{
|
||||
if (value != _showLabels)
|
||||
{
|
||||
_showLabels = value;
|
||||
_dockSettingsModel.ShowLabels = value switch
|
||||
{
|
||||
ShowLabelsOption.Default => null,
|
||||
ShowLabelsOption.ShowLabels => true,
|
||||
ShowLabelsOption.HideLabels => false,
|
||||
_ => null,
|
||||
};
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ShowLabelsOption FetchShowLabels()
|
||||
{
|
||||
if (_dockSettingsModel.ShowLabels == null)
|
||||
{
|
||||
return ShowLabelsOption.Default;
|
||||
}
|
||||
|
||||
return _dockSettingsModel.ShowLabels.Value ? ShowLabelsOption.ShowLabels : ShowLabelsOption.HideLabels;
|
||||
}
|
||||
|
||||
// used to map to ComboBox selection
|
||||
public int ShowLabelsIndex
|
||||
{
|
||||
get => (int)ShowLabels;
|
||||
set => ShowLabels = (ShowLabelsOption)value;
|
||||
}
|
||||
|
||||
private DockPinSide PinSide
|
||||
{
|
||||
get => _pinSide;
|
||||
set
|
||||
{
|
||||
if (value != _pinSide)
|
||||
{
|
||||
UpdatePinSide(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DockPinSide _pinSide;
|
||||
|
||||
public int PinSideIndex
|
||||
{
|
||||
get => (int)PinSide;
|
||||
set => PinSide = (DockPinSide)value;
|
||||
}
|
||||
|
||||
public DockBandSettingsViewModel(
|
||||
DockBandSettings dockSettingsModel,
|
||||
TopLevelViewModel topLevelAdapter,
|
||||
DockBandViewModel? bandViewModel,
|
||||
SettingsModel settingsModel)
|
||||
{
|
||||
_dockSettingsModel = dockSettingsModel;
|
||||
_adapter = topLevelAdapter;
|
||||
_bandViewModel = bandViewModel;
|
||||
_settingsModel = settingsModel;
|
||||
_pinSide = FetchPinSide();
|
||||
_showLabels = FetchShowLabels();
|
||||
}
|
||||
|
||||
private DockPinSide FetchPinSide()
|
||||
{
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
var inStart = dockSettings.StartBands.Any(b => b.Id == _dockSettingsModel.Id);
|
||||
if (inStart)
|
||||
{
|
||||
return DockPinSide.Start;
|
||||
}
|
||||
|
||||
var inEnd = dockSettings.EndBands.Any(b => b.Id == _dockSettingsModel.Id);
|
||||
if (inEnd)
|
||||
{
|
||||
return DockPinSide.End;
|
||||
}
|
||||
|
||||
return DockPinSide.None;
|
||||
}
|
||||
|
||||
private int NumItemsInBand()
|
||||
{
|
||||
var bandVm = _bandViewModel;
|
||||
if (bandVm is null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return _bandViewModel!.Items.Count;
|
||||
}
|
||||
|
||||
private void Save()
|
||||
{
|
||||
SettingsModel.SaveSettings(_settingsModel);
|
||||
}
|
||||
|
||||
private void UpdatePinSide(DockPinSide value)
|
||||
{
|
||||
OnPinSideChanged(value);
|
||||
OnPropertyChanged(nameof(PinSideIndex));
|
||||
OnPropertyChanged(nameof(PinSide));
|
||||
}
|
||||
|
||||
public void SetBandPosition(DockPinSide side, int? index)
|
||||
{
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
// Remove from both sides first
|
||||
dockSettings.StartBands.RemoveAll(b => b.Id == _dockSettingsModel.Id);
|
||||
dockSettings.EndBands.RemoveAll(b => b.Id == _dockSettingsModel.Id);
|
||||
|
||||
// Add to the selected side
|
||||
switch (side)
|
||||
{
|
||||
case DockPinSide.Start:
|
||||
{
|
||||
var insertIndex = index ?? dockSettings.StartBands.Count;
|
||||
dockSettings.StartBands.Insert(insertIndex, _dockSettingsModel);
|
||||
break;
|
||||
}
|
||||
|
||||
case DockPinSide.End:
|
||||
{
|
||||
var insertIndex = index ?? dockSettings.EndBands.Count;
|
||||
dockSettings.EndBands.Insert(insertIndex, _dockSettingsModel);
|
||||
break;
|
||||
}
|
||||
|
||||
case DockPinSide.None:
|
||||
default:
|
||||
// Do nothing
|
||||
break;
|
||||
}
|
||||
|
||||
Save();
|
||||
}
|
||||
|
||||
private void OnPinSideChanged(DockPinSide value)
|
||||
{
|
||||
SetBandPosition(value, null);
|
||||
}
|
||||
}
|
||||
|
||||
public enum DockPinSide
|
||||
{
|
||||
None,
|
||||
Start,
|
||||
End,
|
||||
}
|
||||
|
||||
public enum ShowLabelsOption
|
||||
{
|
||||
Default,
|
||||
ShowLabels,
|
||||
HideLabels,
|
||||
}
|
||||
|
||||
#pragma warning restore SA1402 // File may only contain a single type
|
||||
@@ -1,130 +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 Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
|
||||
#pragma warning disable SA1402 // File may only contain a single type
|
||||
|
||||
public sealed partial class DockBandViewModel : ExtensionObjectViewModel
|
||||
{
|
||||
private readonly CommandItemViewModel _rootItem;
|
||||
|
||||
public ObservableCollection<DockItemViewModel> Items { get; } = new();
|
||||
|
||||
private bool _showLabels = true;
|
||||
|
||||
public string Id => _rootItem.Command.Id;
|
||||
|
||||
internal DockBandViewModel(
|
||||
CommandItemViewModel commandItemViewModel,
|
||||
WeakReference<IPageContext> errorContext,
|
||||
DockBandSettings settings,
|
||||
DockSettings dockSettings)
|
||||
: base(errorContext)
|
||||
{
|
||||
_rootItem = commandItemViewModel;
|
||||
|
||||
_showLabels = settings.ResolveShowLabels(dockSettings.ShowLabels);
|
||||
}
|
||||
|
||||
private void InitializeFromList(IListPage list)
|
||||
{
|
||||
var items = list.GetItems();
|
||||
var newViewModels = new List<DockItemViewModel>();
|
||||
foreach (var item in items)
|
||||
{
|
||||
var newItemVm = new DockItemViewModel(new(item), this.PageContext, _showLabels);
|
||||
newItemVm.SlowInitializeProperties();
|
||||
newViewModels.Add(newItemVm);
|
||||
}
|
||||
|
||||
DoOnUiThread(() =>
|
||||
{
|
||||
ListHelpers.InPlaceUpdateList(Items, newViewModels, out var removed);
|
||||
});
|
||||
|
||||
// TODO! dispose removed VMs
|
||||
}
|
||||
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
var command = _rootItem.Command;
|
||||
var list = command.Model.Unsafe as IListPage;
|
||||
if (list is not null)
|
||||
{
|
||||
InitializeFromList(list);
|
||||
list.ItemsChanged += HandleItemsChanged;
|
||||
}
|
||||
else
|
||||
{
|
||||
DoOnUiThread(() =>
|
||||
{
|
||||
var dockItem = new DockItemViewModel(_rootItem, _showLabels);
|
||||
dockItem.SlowInitializeProperties();
|
||||
Items.Add(dockItem);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleItemsChanged(object sender, IItemsChangedEventArgs args)
|
||||
{
|
||||
if (_rootItem.Command.Model.Unsafe is IListPage p)
|
||||
{
|
||||
InitializeFromList(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public partial class DockItemViewModel : CommandItemViewModel
|
||||
{
|
||||
private bool _showLabel = true;
|
||||
|
||||
public bool ShowLabel
|
||||
{
|
||||
get => _showLabel;
|
||||
internal set
|
||||
{
|
||||
_showLabel = value;
|
||||
UpdateProperty(nameof(HasText));
|
||||
}
|
||||
}
|
||||
|
||||
public override string Title => ItemTitle;
|
||||
|
||||
public override bool HasText => _showLabel ? base.HasText : false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tooltip for the dock item, which includes the title and
|
||||
/// subtitle. If it doesn't have one part, it just returns the other.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Trickery: in the case one is empty, we can just concatenate, and it will
|
||||
/// always only be the one that's non-empty
|
||||
/// </remarks>
|
||||
public string Tooltip =>
|
||||
!string.IsNullOrEmpty(Title) && !string.IsNullOrEmpty(Subtitle) ?
|
||||
$"{Title}\n{Subtitle}" :
|
||||
Title + Subtitle;
|
||||
|
||||
public DockItemViewModel(CommandItemViewModel root, bool showLabel)
|
||||
: this(root.Model, root.PageContext, showLabel)
|
||||
{
|
||||
}
|
||||
|
||||
public DockItemViewModel(ExtensionObject<ICommandItem> item, WeakReference<IPageContext> errorContext, bool showLabel)
|
||||
: base(item, errorContext)
|
||||
{
|
||||
_showLabel = showLabel;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma warning restore SA1402 // File may only contain a single type
|
||||
@@ -1,185 +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.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.Common;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
|
||||
public sealed partial class DockViewModel : IDisposable,
|
||||
IRecipient<CommandsReloadedMessage>,
|
||||
IPageContext
|
||||
{
|
||||
private readonly TopLevelCommandManager _topLevelCommandManager;
|
||||
|
||||
private DockSettings _settings;
|
||||
|
||||
// private DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
// private DispatcherQueue _updateWindowsQueue = DispatcherQueueController.CreateOnDedicatedThread().DispatcherQueue;
|
||||
public TaskScheduler Scheduler { get; }
|
||||
|
||||
public ObservableCollection<DockBandViewModel> StartItems { get; } = new();
|
||||
|
||||
public ObservableCollection<DockBandViewModel> EndItems { get; } = new();
|
||||
|
||||
public ObservableCollection<TopLevelViewModel> AllItems => _topLevelCommandManager.DockBands;
|
||||
|
||||
public DockViewModel(
|
||||
TopLevelCommandManager tlcManager,
|
||||
SettingsModel settings,
|
||||
TaskScheduler scheduler)
|
||||
{
|
||||
_topLevelCommandManager = tlcManager;
|
||||
_settings = settings.DockSettings;
|
||||
Scheduler = scheduler;
|
||||
WeakReferenceMessenger.Default.Register<CommandsReloadedMessage>(this);
|
||||
}
|
||||
|
||||
public void UpdateSettings(DockSettings settings)
|
||||
{
|
||||
Logger.LogDebug($"DockViewModel.UpdateSettings");
|
||||
_settings = settings;
|
||||
SetupBands();
|
||||
}
|
||||
|
||||
private void SetupBands()
|
||||
{
|
||||
Logger.LogDebug($"Setting up dock bands");
|
||||
SetupBands(_settings.StartBands, StartItems);
|
||||
SetupBands(_settings.EndBands, EndItems);
|
||||
}
|
||||
|
||||
private void SetupBands(
|
||||
List<DockBandSettings> bands,
|
||||
ObservableCollection<DockBandViewModel> target)
|
||||
{
|
||||
List<DockBandViewModel> newBands = new();
|
||||
foreach (var band in bands)
|
||||
{
|
||||
var commandId = band.Id;
|
||||
var topLevelCommand = _topLevelCommandManager.LookupDockBand(commandId);
|
||||
|
||||
if (topLevelCommand is null)
|
||||
{
|
||||
Logger.LogWarning($"Failed to find band {commandId}");
|
||||
}
|
||||
|
||||
if (topLevelCommand is not null)
|
||||
{
|
||||
var bandVm = CreateBandItem(band, topLevelCommand.ItemViewModel);
|
||||
newBands.Add(bandVm);
|
||||
}
|
||||
}
|
||||
|
||||
var beforeCount = target.Count;
|
||||
var afterCount = newBands.Count;
|
||||
|
||||
DoOnUiThread(() =>
|
||||
{
|
||||
ListHelpers.InPlaceUpdateList(target, newBands, out var removed);
|
||||
var isStartBand = target == StartItems;
|
||||
var label = isStartBand ? "Start bands:" : "End bands:";
|
||||
Logger.LogDebug($"{label} ({beforeCount}) -> ({afterCount}), Removed {removed?.Count ?? 0} items");
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public void Receive(CommandsReloadedMessage message)
|
||||
{
|
||||
SetupBands();
|
||||
CoreLogger.LogDebug("Bands reloaded");
|
||||
}
|
||||
|
||||
private DockBandViewModel CreateBandItem(
|
||||
DockBandSettings bandSettings,
|
||||
CommandItemViewModel commandItem)
|
||||
{
|
||||
DockBandViewModel band = new(commandItem, new(this), bandSettings, _settings);
|
||||
band.InitializeProperties(); // TODO! make async
|
||||
return band;
|
||||
}
|
||||
|
||||
public DockBandViewModel? FindBandByTopLevel(TopLevelViewModel tlc)
|
||||
{
|
||||
var id = tlc.Id;
|
||||
return FindBandById(id);
|
||||
}
|
||||
|
||||
public DockBandViewModel? FindBandById(string id)
|
||||
{
|
||||
foreach (var band in StartItems)
|
||||
{
|
||||
if (band.Id == id)
|
||||
{
|
||||
return band;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var band in EndItems)
|
||||
{
|
||||
if (band.Id == id)
|
||||
{
|
||||
return band;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void ShowException(Exception ex, string? extensionHint = null)
|
||||
{
|
||||
var extensionText = extensionHint ?? "<unknown>";
|
||||
CoreLogger.LogError($"Error in extension {extensionText}", ex);
|
||||
}
|
||||
|
||||
private void DoOnUiThread(Action action)
|
||||
{
|
||||
Task.Factory.StartNew(
|
||||
action,
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.None,
|
||||
Scheduler);
|
||||
}
|
||||
|
||||
public CommandItemViewModel GetContextMenuForDock()
|
||||
{
|
||||
var model = new DockContextMenuItem();
|
||||
var vm = new CommandItemViewModel(new(model), new(this));
|
||||
vm.SlowInitializeProperties();
|
||||
return vm;
|
||||
}
|
||||
|
||||
private sealed partial class DockContextMenuItem : CommandItem
|
||||
{
|
||||
public DockContextMenuItem()
|
||||
{
|
||||
var openSettingsCommand = new AnonymousCommand(
|
||||
action: () =>
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new OpenSettingsMessage("Dock"));
|
||||
})
|
||||
{
|
||||
Name = "Customize", // TODO!Loc
|
||||
Icon = Icons.SettingsIcon,
|
||||
};
|
||||
|
||||
MoreCommands = new CommandContextItem[]
|
||||
{
|
||||
new CommandContextItem(openSettingsCommand),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning restore SA1402 // File may only contain a single type
|
||||
@@ -1,16 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
internal sealed class Icons
|
||||
{
|
||||
internal static IconInfo PinIcon => new("\uE718"); // Pin icon
|
||||
|
||||
internal static IconInfo UnpinIcon => new("\uE77A"); // Unpin icon
|
||||
|
||||
internal static IconInfo SettingsIcon => new("\uE713"); // Settings icon
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
public record CommandsReloadedMessage();
|
||||
@@ -4,6 +4,6 @@
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Messages;
|
||||
|
||||
public record OpenSettingsMessage(string? Page = null)
|
||||
public record OpenSettingsMessage()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
public record ShowHideDockMessage(bool ShowDock);
|
||||
@@ -60,15 +60,6 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Open Command Palette.
|
||||
/// </summary>
|
||||
public static string builtin_command_palette_title {
|
||||
get {
|
||||
return ResourceManager.GetString("builtin_command_palette_title", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Create another.
|
||||
/// </summary>
|
||||
@@ -294,15 +285,6 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Built-in.
|
||||
/// </summary>
|
||||
public static string builtin_extension_name_fallback {
|
||||
get {
|
||||
return ResourceManager.GetString("builtin_extension_name_fallback", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Home.
|
||||
/// </summary>
|
||||
@@ -447,42 +429,6 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} items.
|
||||
/// </summary>
|
||||
public static string dock_item_count_plural {
|
||||
get {
|
||||
return ResourceManager.GetString("dock_item_count_plural", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to 1 item.
|
||||
/// </summary>
|
||||
public static string dock_item_count_singular {
|
||||
get {
|
||||
return ResourceManager.GetString("dock_item_count_singular", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Pin to dock.
|
||||
/// </summary>
|
||||
public static string dock_pin_command_name {
|
||||
get {
|
||||
return ResourceManager.GetString("dock_pin_command_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Unpin from dock.
|
||||
/// </summary>
|
||||
public static string dock_unpin_command_name {
|
||||
get {
|
||||
return ResourceManager.GetString("dock_unpin_command_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Fallbacks.
|
||||
/// </summary>
|
||||
|
||||
@@ -239,30 +239,6 @@
|
||||
<data name="builtin_settings_extension_n_extensions_installed" xml:space="preserve">
|
||||
<value>{0} extensions installed</value>
|
||||
</data>
|
||||
<data name="builtin_extension_name_fallback" xml:space="preserve">
|
||||
<value>Built-in</value>
|
||||
<comment>Fallback name for built-in extensions</comment>
|
||||
</data>
|
||||
<data name="dock_pin_command_name" xml:space="preserve">
|
||||
<value>Pin to dock</value>
|
||||
<comment>Command name for pinning an item to the dock</comment>
|
||||
</data>
|
||||
<data name="dock_unpin_command_name" xml:space="preserve">
|
||||
<value>Unpin from dock</value>
|
||||
<comment>Command name for unpinning an item from the dock</comment>
|
||||
</data>
|
||||
<data name="dock_item_count_singular" xml:space="preserve">
|
||||
<value>1 item</value>
|
||||
<comment>Singular form for item count in dock band</comment>
|
||||
</data>
|
||||
<data name="dock_item_count_plural" xml:space="preserve">
|
||||
<value>{0} items</value>
|
||||
<comment>Plural form for item count in dock band</comment>
|
||||
</data>
|
||||
<data name="builtin_command_palette_title" xml:space="preserve">
|
||||
<value>Open Command Palette</value>
|
||||
<comment>Title for the command to open the command palette</comment>
|
||||
</data>
|
||||
<data name="builtin_settings_appearance_pick_background_image_title" xml:space="preserve">
|
||||
<value>Pick background image</value>
|
||||
</data>
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
|
||||
#pragma warning disable SA1402 // File may only contain a single type
|
||||
|
||||
/// <summary>
|
||||
/// Settings for the Dock. These are settings for _the whole dock_. Band-specific
|
||||
/// settings are in <see cref="DockBandSettings"/>.
|
||||
/// </summary>
|
||||
public class DockSettings
|
||||
{
|
||||
public DockSide Side { get; set; } = DockSide.Top;
|
||||
|
||||
public DockSize DockSize { get; set; } = DockSize.Small;
|
||||
|
||||
public DockSize DockIconsSize { get; set; } = DockSize.Small;
|
||||
|
||||
public DockBackdrop Backdrop { get; set; } = DockBackdrop.Acrylic;
|
||||
|
||||
public List<string> PinnedCommands { get; set; } = [];
|
||||
|
||||
public List<DockBandSettings> StartBands { get; set; } = [];
|
||||
|
||||
public List<DockBandSettings> EndBands { get; set; } = [];
|
||||
|
||||
public bool ShowLabels { get; set; } = true;
|
||||
|
||||
public DockSettings()
|
||||
{
|
||||
// Initialize with default values
|
||||
PinnedCommands = [
|
||||
"com.microsoft.cmdpal.winget"
|
||||
];
|
||||
|
||||
StartBands.Add(new DockBandSettings { Id = "com.microsoft.cmdpal.home" });
|
||||
StartBands.Add(new DockBandSettings { Id = "com.microsoft.cmdpal.winget", ShowLabels = false });
|
||||
|
||||
EndBands.Add(new DockBandSettings { Id = "com.microsoft.cmdpal.performanceWidget" });
|
||||
EndBands.Add(new DockBandSettings { Id = "com.microsoft.cmdpal.timedate.dockband" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Settings for a specific dock band. These are per-band settings stored
|
||||
/// within the overall <see cref="DockSettings"/>.
|
||||
/// </summary>
|
||||
public class DockBandSettings
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
public bool? ShowLabels { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the effective value of <see cref="ShowLabels"/> for this band.
|
||||
/// If this band doesn't have a specific value set, we'll fall back to the
|
||||
/// dock-wide setting (passed as <paramref name="defaultValue"/>).
|
||||
/// </summary>
|
||||
public bool ResolveShowLabels(bool defaultValue) => ShowLabels ?? defaultValue;
|
||||
}
|
||||
|
||||
public enum DockSide
|
||||
{
|
||||
Left = 0,
|
||||
Top = 1,
|
||||
Right = 2,
|
||||
Bottom = 3,
|
||||
}
|
||||
|
||||
public enum DockSize
|
||||
{
|
||||
Small,
|
||||
Medium,
|
||||
Large,
|
||||
}
|
||||
|
||||
public enum DockBackdrop
|
||||
{
|
||||
Mica,
|
||||
Transparent,
|
||||
Acrylic,
|
||||
}
|
||||
|
||||
#pragma warning restore SA1402 // File may only contain a single type
|
||||
@@ -66,10 +66,6 @@ public partial class SettingsModel : ObservableObject
|
||||
|
||||
public EscapeKeyBehavior EscapeKeyBehaviorSetting { get; set; } = EscapeKeyBehavior.ClearSearchFirstThenGoBack;
|
||||
|
||||
public bool EnableDock { get; set; }
|
||||
|
||||
public DockSettings DockSettings { get; set; } = new();
|
||||
|
||||
public UserTheme Theme { get; set; } = UserTheme.Default;
|
||||
|
||||
public ColorizationMode ColorizationMode { get; set; }
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Core.Common.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
@@ -176,58 +173,6 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
public DockSide Dock_Side
|
||||
{
|
||||
get => _settings.DockSettings.Side;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.Side = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public DockSize Dock_DockSize
|
||||
{
|
||||
get => _settings.DockSettings.DockSize;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.DockSize = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public DockBackdrop Dock_Backdrop
|
||||
{
|
||||
get => _settings.DockSettings.Backdrop;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.Backdrop = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public bool Dock_ShowLabels
|
||||
{
|
||||
get => _settings.DockSettings.ShowLabels;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.ShowLabels = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public bool EnableDock
|
||||
{
|
||||
get => _settings.EnableDock;
|
||||
set
|
||||
{
|
||||
_settings.EnableDock = value;
|
||||
Save();
|
||||
WeakReferenceMessenger.Default.Send(new ShowHideDockMessage(value));
|
||||
WeakReferenceMessenger.Default.Send(new ReloadCommandsMessage()); // TODO! we need to update the MoreCommands of all top level items, but we don't _really_ want to reload
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<ProviderSettingsViewModel> CommandProviders { get; } = new();
|
||||
|
||||
public ObservableCollection<FallbackSettingsViewModel> FallbackRankings { get; set; } = new();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -44,8 +44,6 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
|
||||
public ObservableCollection<TopLevelViewModel> TopLevelCommands { get; set; } = [];
|
||||
|
||||
public ObservableCollection<TopLevelViewModel> DockBands { get; set; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsLoading { get; private set; } = true;
|
||||
|
||||
@@ -81,23 +79,12 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
_builtInCommands.Add(wrapper);
|
||||
}
|
||||
|
||||
var objects = await LoadTopLevelCommandsFromProvider(wrapper);
|
||||
var commands = await LoadTopLevelCommandsFromProvider(wrapper);
|
||||
lock (TopLevelCommands)
|
||||
{
|
||||
if (objects.Commands is IEnumerable<TopLevelViewModel> commands)
|
||||
foreach (var c in commands)
|
||||
{
|
||||
foreach (var c in commands)
|
||||
{
|
||||
TopLevelCommands.Add(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (objects.DockBands is IEnumerable<TopLevelViewModel> bands)
|
||||
{
|
||||
foreach (var c in bands)
|
||||
{
|
||||
DockBands.Add(c);
|
||||
}
|
||||
TopLevelCommands.Add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,7 +97,7 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
}
|
||||
|
||||
// May be called from a background thread
|
||||
private async Task<TopLevelObjectSets> LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
|
||||
private async Task<IEnumerable<TopLevelViewModel>> LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
|
||||
{
|
||||
WeakReference<IPageContext> weakSelf = new(this);
|
||||
|
||||
@@ -120,7 +107,6 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
() =>
|
||||
{
|
||||
List<TopLevelViewModel> commands = [];
|
||||
List<TopLevelViewModel> bands = [];
|
||||
foreach (var item in commandProvider.TopLevelItems)
|
||||
{
|
||||
commands.Add(item);
|
||||
@@ -134,15 +120,7 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var item in commandProvider.DockBandItems)
|
||||
{
|
||||
bands.Add(item);
|
||||
}
|
||||
|
||||
var commandsCount = commands.Count;
|
||||
var bandsCount = bands.Count;
|
||||
Logger.LogDebug($"{commandProvider.ProviderId}: Loaded {commandsCount} commands, {bandsCount} bands");
|
||||
return new TopLevelObjectSets(commands, bands);
|
||||
return commands;
|
||||
},
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.None,
|
||||
@@ -182,8 +160,6 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
}
|
||||
}
|
||||
|
||||
List<TopLevelViewModel> newBands = [.. sender.DockBandItems];
|
||||
|
||||
// modify the TopLevelCommands under shared lock; event if we clone it, we don't want
|
||||
// TopLevelCommands to get modified while we're working on it. Otherwise, we might
|
||||
// out clone would be stale at the end of this method.
|
||||
@@ -200,13 +176,6 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
clone.InsertRange(startIndex, newItems);
|
||||
|
||||
ListHelpers.InPlaceUpdateList(TopLevelCommands, clone);
|
||||
|
||||
// same idea for DockBands
|
||||
List<TopLevelViewModel> dockClone = [.. DockBands];
|
||||
var dockStartIndex = FindIndexForFirstProviderItem(dockClone, sender.ProviderId);
|
||||
dockClone.RemoveAll(item => item.CommandProviderId == sender.ProviderId);
|
||||
dockClone.InsertRange(dockStartIndex, newBands);
|
||||
ListHelpers.InPlaceUpdateList(DockBands, dockClone);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -253,7 +222,6 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
lock (TopLevelCommands)
|
||||
{
|
||||
TopLevelCommands.Clear();
|
||||
DockBands.Clear();
|
||||
}
|
||||
|
||||
await LoadBuiltinsAsync();
|
||||
@@ -332,34 +300,17 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
|
||||
lock (TopLevelCommands)
|
||||
{
|
||||
foreach (var providerObjects in commandSets)
|
||||
foreach (var commands in commandSets)
|
||||
{
|
||||
var commandsCount = providerObjects.Commands?.Count() ?? 0;
|
||||
var bandsCount = providerObjects.DockBands?.Count() ?? 0;
|
||||
Logger.LogDebug($"(some provider) Loaded {commandsCount} commands and {bandsCount} bands");
|
||||
|
||||
if (providerObjects.Commands is IEnumerable<TopLevelViewModel> commands)
|
||||
foreach (var c in commands)
|
||||
{
|
||||
foreach (var c in commands)
|
||||
{
|
||||
TopLevelCommands.Add(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (providerObjects.DockBands is IEnumerable<TopLevelViewModel> bands)
|
||||
{
|
||||
foreach (var c in bands)
|
||||
{
|
||||
DockBands.Add(c);
|
||||
}
|
||||
TopLevelCommands.Add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
timer.Stop();
|
||||
Logger.LogDebug($"Loading extensions took {timer.ElapsedMilliseconds} ms");
|
||||
|
||||
WeakReferenceMessenger.Default.Send<CommandsReloadedMessage>();
|
||||
}
|
||||
|
||||
private async Task<CommandProviderWrapper?> StartExtensionWithTimeoutAsync(IExtensionWrapper extension)
|
||||
@@ -377,9 +328,7 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
}
|
||||
}
|
||||
|
||||
private record TopLevelObjectSets(IEnumerable<TopLevelViewModel>? Commands, IEnumerable<TopLevelViewModel>? DockBands);
|
||||
|
||||
private async Task<TopLevelObjectSets?> LoadCommandsWithTimeoutAsync(CommandProviderWrapper wrapper)
|
||||
private async Task<IEnumerable<TopLevelViewModel>?> LoadCommandsWithTimeoutAsync(CommandProviderWrapper wrapper)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -459,23 +408,6 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
return null;
|
||||
}
|
||||
|
||||
public TopLevelViewModel? LookupDockBand(string id)
|
||||
{
|
||||
// TODO! bad that we're using TopLevelCommands as the object to lock, even for bands
|
||||
lock (TopLevelCommands)
|
||||
{
|
||||
foreach (var command in DockBands)
|
||||
{
|
||||
if (command.Id == id)
|
||||
{
|
||||
return command;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Receive(ReloadCommandsMessage message) =>
|
||||
ReloadAllCommandsAsync().ConfigureAwait(false);
|
||||
|
||||
@@ -494,41 +426,6 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
}
|
||||
}
|
||||
|
||||
internal void PinDockBand(TopLevelViewModel bandVm)
|
||||
{
|
||||
lock (DockBands)
|
||||
{
|
||||
foreach (var existing in DockBands)
|
||||
{
|
||||
if (existing.Id == bandVm.Id)
|
||||
{
|
||||
// already pinned
|
||||
Logger.LogDebug($"Dock band '{bandVm.Id}' is already pinned.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogDebug($"Attempting to pin dock band '{bandVm.Id}' from provider '{bandVm.CommandProviderId}'.");
|
||||
var providerId = bandVm.CommandProviderId;
|
||||
var foundProvider = false;
|
||||
foreach (var provider in CommandProviders)
|
||||
{
|
||||
if (provider.Id == providerId)
|
||||
{
|
||||
Logger.LogDebug($"Found provider '{providerId}' to pin dock band '{bandVm.Id}'.");
|
||||
provider.PinDockBand(bandVm);
|
||||
foundProvider = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundProvider)
|
||||
{
|
||||
Logger.LogWarning($"Could not find provider '{providerId}' to pin dock band '{bandVm.Id}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_reloadCommandsGate.Dispose();
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
@@ -24,7 +22,6 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
private readonly ProviderSettings _providerSettings;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly CommandItemViewModel _commandItemViewModel;
|
||||
private readonly DockViewModel? _dockViewModel;
|
||||
|
||||
private readonly string _commandProviderId;
|
||||
|
||||
@@ -48,28 +45,39 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
|
||||
public CommandPaletteHost ExtensionHost { get; private set; }
|
||||
|
||||
public string ExtensionName => ExtensionHost.Extension?.ExtensionDisplayName ?? Properties.Resources.builtin_extension_name_fallback;
|
||||
|
||||
public CommandViewModel CommandViewModel => _commandItemViewModel.Command;
|
||||
|
||||
public CommandItemViewModel ItemViewModel => _commandItemViewModel;
|
||||
|
||||
public string CommandProviderId => _commandProviderId;
|
||||
|
||||
public IconInfoViewModel IconViewModel => _commandItemViewModel.Icon;
|
||||
|
||||
////// ICommandItem
|
||||
public string Title => _commandItemViewModel.Title;
|
||||
|
||||
public string Subtitle => _commandItemViewModel.Subtitle;
|
||||
|
||||
public IIconInfo Icon => (IIconInfo)IconViewModel;
|
||||
public IIconInfo Icon => _commandItemViewModel.Icon;
|
||||
|
||||
public IIconInfo InitialIcon => _initialIcon ?? _commandItemViewModel.Icon;
|
||||
|
||||
ICommand? ICommandItem.Command => _commandItemViewModel.Command.Model.Unsafe;
|
||||
|
||||
IContextItem?[] ICommandItem.MoreCommands => BuildContextMenu();
|
||||
IContextItem?[] ICommandItem.MoreCommands => _commandItemViewModel.MoreCommands
|
||||
.Select(item =>
|
||||
{
|
||||
if (item is ISeparatorContextItem)
|
||||
{
|
||||
return item as IContextItem;
|
||||
}
|
||||
else if (item is CommandContextItemViewModel commandItem)
|
||||
{
|
||||
return commandItem.Model.Unsafe;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}).ToArray();
|
||||
|
||||
////// IListItem
|
||||
ITag[] IListItem.Tags => Tags.ToArray();
|
||||
@@ -168,37 +176,9 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
}
|
||||
}
|
||||
|
||||
// Dock properties
|
||||
public bool IsDockBand { get; private set; }
|
||||
|
||||
public DockBandSettings? DockBandSettings
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsDockBand)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var bandSettings = _settings.DockSettings.StartBands
|
||||
.Concat(_settings.DockSettings.EndBands)
|
||||
.FirstOrDefault(band => band.Id == this.Id);
|
||||
if (bandSettings is null)
|
||||
{
|
||||
return new DockBandSettings()
|
||||
{
|
||||
Id = this.Id,
|
||||
ShowLabels = true,
|
||||
};
|
||||
}
|
||||
|
||||
return bandSettings;
|
||||
}
|
||||
}
|
||||
|
||||
public TopLevelViewModel(
|
||||
CommandItemViewModel item,
|
||||
TopLevelType topLevelType,
|
||||
bool isFallback,
|
||||
CommandPaletteHost extensionHost,
|
||||
string commandProviderId,
|
||||
SettingsModel settings,
|
||||
@@ -212,23 +192,23 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
_commandProviderId = commandProviderId;
|
||||
_commandItemViewModel = item;
|
||||
|
||||
IsFallback = topLevelType == TopLevelType.Fallback;
|
||||
IsDockBand = topLevelType == TopLevelType.DockBand;
|
||||
IsFallback = isFallback;
|
||||
ExtensionHost = extensionHost;
|
||||
if (IsFallback && commandItem is FallbackCommandItem fallback)
|
||||
if (isFallback && commandItem is FallbackCommandItem fallback)
|
||||
{
|
||||
_fallbackId = fallback.Id;
|
||||
}
|
||||
|
||||
item.PropertyChanged += Item_PropertyChanged;
|
||||
|
||||
_dockViewModel = serviceProvider.GetService<DockViewModel>();
|
||||
// UpdateAlias();
|
||||
// UpdateHotkey();
|
||||
// UpdateTags();
|
||||
}
|
||||
|
||||
internal void InitializeProperties()
|
||||
{
|
||||
ItemViewModel.SlowInitializeProperties();
|
||||
GenerateId();
|
||||
|
||||
if (IsFallback)
|
||||
{
|
||||
@@ -280,7 +260,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
return;
|
||||
}
|
||||
|
||||
_initialIcon = (IIconInfo?)_commandItemViewModel.Icon;
|
||||
_initialIcon = _commandItemViewModel.Icon;
|
||||
|
||||
if (raiseNotification)
|
||||
{
|
||||
@@ -440,151 +420,4 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
[WellKnownExtensionAttributes.DataPackage] = _commandItemViewModel?.DataPackage,
|
||||
};
|
||||
}
|
||||
|
||||
private IContextItem?[] BuildContextMenu()
|
||||
{
|
||||
List<IContextItem?> contextItems = new();
|
||||
|
||||
foreach (var item in _commandItemViewModel.MoreCommands)
|
||||
{
|
||||
if (item is ISeparatorContextItem)
|
||||
{
|
||||
contextItems.Add(item as IContextItem);
|
||||
}
|
||||
else if (item is CommandContextItemViewModel commandItem)
|
||||
{
|
||||
contextItems.Add(commandItem.Model.Unsafe);
|
||||
}
|
||||
}
|
||||
|
||||
var dockEnabled = _settings.EnableDock;
|
||||
if (dockEnabled && _dockViewModel is not null)
|
||||
{
|
||||
// Add a separator
|
||||
contextItems.Add(new Separator());
|
||||
|
||||
var inStartBands = _settings.DockSettings.StartBands.Any(band => band.Id == this.Id);
|
||||
var inEndBands = _settings.DockSettings.EndBands.Any(band => band.Id == this.Id);
|
||||
var alreadyPinned = (inStartBands || inEndBands) &&
|
||||
_settings.DockSettings.PinnedCommands.Contains(this.Id);
|
||||
|
||||
var pinCommand = new PinToDockCommand(
|
||||
this,
|
||||
!alreadyPinned,
|
||||
_dockViewModel,
|
||||
_settings,
|
||||
_serviceProvider.GetService<TopLevelCommandManager>()!);
|
||||
|
||||
var contextItem = new CommandContextItem(pinCommand);
|
||||
|
||||
contextItems.Add(contextItem);
|
||||
}
|
||||
|
||||
return contextItems.ToArray();
|
||||
}
|
||||
|
||||
internal ICommandItem ToPinnedDockBandItem()
|
||||
{
|
||||
var item = new PinnedDockItem(item: this, id: Id);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
internal TopLevelViewModel CloneAsBand()
|
||||
{
|
||||
return new TopLevelViewModel(
|
||||
_commandItemViewModel,
|
||||
TopLevelType.DockBand,
|
||||
ExtensionHost,
|
||||
_commandProviderId,
|
||||
_settings,
|
||||
_providerSettings,
|
||||
_serviceProvider,
|
||||
_commandItemViewModel.Model.Unsafe);
|
||||
}
|
||||
|
||||
private sealed partial class PinToDockCommand : InvokableCommand
|
||||
{
|
||||
private readonly TopLevelViewModel _topLevelViewModel;
|
||||
private readonly DockViewModel _dockViewModel;
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly TopLevelCommandManager _topLevelCommandManager;
|
||||
private readonly bool _pin;
|
||||
|
||||
public override IconInfo Icon => _pin ? Icons.PinIcon : Icons.UnpinIcon;
|
||||
|
||||
public override string Name => _pin ? Properties.Resources.dock_pin_command_name : Properties.Resources.dock_unpin_command_name;
|
||||
|
||||
public PinToDockCommand(
|
||||
TopLevelViewModel topLevelViewModel,
|
||||
bool pin,
|
||||
DockViewModel dockViewModel,
|
||||
SettingsModel settings,
|
||||
TopLevelCommandManager topLevelCommandManager)
|
||||
{
|
||||
_topLevelViewModel = topLevelViewModel;
|
||||
_dockViewModel = dockViewModel;
|
||||
_settings = settings;
|
||||
_topLevelCommandManager = topLevelCommandManager;
|
||||
_pin = pin;
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
Logger.LogDebug($"PinToDockCommand.Invoke({_pin}): {_topLevelViewModel.Id}");
|
||||
if (_pin)
|
||||
{
|
||||
PinToDock();
|
||||
}
|
||||
else
|
||||
{
|
||||
UnpinFromDock();
|
||||
}
|
||||
|
||||
// Notify that the MoreCommands have changed, so the context menu updates
|
||||
_topLevelViewModel.PropChanged?.Invoke(
|
||||
_topLevelViewModel,
|
||||
new PropChangedEventArgs(nameof(ICommandItem.MoreCommands)));
|
||||
return CommandResult.GoHome();
|
||||
}
|
||||
|
||||
private void PinToDock()
|
||||
{
|
||||
// TODO! Deal with "the command ID is already pinned in PinnedCommands but not in one of StartBands/EndBands"
|
||||
if (!_settings.DockSettings.PinnedCommands.Contains(_topLevelViewModel.Id))
|
||||
{
|
||||
_settings.DockSettings.PinnedCommands.Add(_topLevelViewModel.Id);
|
||||
}
|
||||
|
||||
_settings.DockSettings.StartBands.Add(new DockBandSettings()
|
||||
{
|
||||
Id = _topLevelViewModel.Id,
|
||||
ShowLabels = true,
|
||||
});
|
||||
|
||||
// Create a new band VM from our current TLVM. This will allow us to
|
||||
// update the bands in the CommandProviderWrapper and the TLCM,
|
||||
// without forcing a whole reload
|
||||
var bandVm = _topLevelViewModel.CloneAsBand();
|
||||
_topLevelCommandManager.PinDockBand(bandVm);
|
||||
|
||||
_topLevelViewModel.Save();
|
||||
}
|
||||
|
||||
private void UnpinFromDock()
|
||||
{
|
||||
_settings.DockSettings.PinnedCommands.Remove(_topLevelViewModel.Id);
|
||||
_settings.DockSettings.StartBands.RemoveAll(band => band.Id == _topLevelViewModel.Id);
|
||||
_settings.DockSettings.EndBands.RemoveAll(band => band.Id == _topLevelViewModel.Id);
|
||||
|
||||
_topLevelViewModel.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum TopLevelType
|
||||
{
|
||||
Normal,
|
||||
Fallback,
|
||||
DockBand,
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI"
|
||||
xmlns:services="using:Microsoft.CmdPal.UI.Services">
|
||||
<Application.Resources>
|
||||
@@ -13,7 +12,6 @@
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
<!-- Other merged dictionaries here -->
|
||||
<ResourceDictionary Source="ms-appx:///Styles/Colors.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Styles/Button.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Styles/TextBlock.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Styles/TextBox.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Styles/Settings.xaml" />
|
||||
@@ -28,14 +26,6 @@
|
||||
|
||||
<x:Double x:Key="SettingActionControlMinWidth">240</x:Double>
|
||||
<Style BasedOn="{StaticResource DefaultCheckBoxStyle}" TargetType="controls:CheckBoxWithDescriptionControl" />
|
||||
|
||||
<converters:StringVisibilityConverter
|
||||
x:Key="StringNotEmptyToVisibilityConverter"
|
||||
EmptyValue="Collapsed"
|
||||
NotEmptyValue="Visible" />
|
||||
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
|
||||
@@ -12,7 +12,6 @@ using Microsoft.CmdPal.Ext.Bookmarks;
|
||||
using Microsoft.CmdPal.Ext.Calc;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory;
|
||||
using Microsoft.CmdPal.Ext.Indexer;
|
||||
using Microsoft.CmdPal.Ext.PerformanceMonitor;
|
||||
using Microsoft.CmdPal.Ext.Registry;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop;
|
||||
using Microsoft.CmdPal.Ext.Shell;
|
||||
@@ -28,7 +27,6 @@ using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
@@ -169,7 +167,6 @@ public partial class App : Application
|
||||
services.AddSingleton<ICommandProvider, TimeDateCommandsProvider>();
|
||||
services.AddSingleton<ICommandProvider, SystemCommandExtensionProvider>();
|
||||
services.AddSingleton<ICommandProvider, RemoteDesktopCommandProvider>();
|
||||
services.AddSingleton<ICommandProvider, PerformanceMonitorCommandsProvider>();
|
||||
}
|
||||
|
||||
private static void AddUIServices(ServiceCollection services)
|
||||
@@ -204,7 +201,6 @@ public partial class App : Application
|
||||
|
||||
// ViewModels
|
||||
services.AddSingleton<ShellViewModel>();
|
||||
services.AddSingleton<DockViewModel>();
|
||||
services.AddSingleton<IPageViewModelFactoryService, CommandPalettePageViewModelFactory>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
<Grid
|
||||
x:Name="IconRoot"
|
||||
Margin="3,0,-5,0"
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsRootPage, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsNested, Mode=OneWay}">
|
||||
<Button
|
||||
x:Name="StatusMessagesButton"
|
||||
x:Uid="StatusMessagesButton"
|
||||
@@ -135,7 +135,7 @@
|
||||
x:Uid="SettingsButton"
|
||||
Click="SettingsIcon_Clicked"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsRootPage, Mode=OneWay}">
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsNested, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon
|
||||
VerticalAlignment="Center"
|
||||
@@ -154,7 +154,7 @@
|
||||
Text="{x:Bind CurrentPageViewModel.Title, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap"
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsRootPage, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}" />
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsNested, Mode=OneWay}" />
|
||||
<StackPanel
|
||||
Grid.Column="2"
|
||||
Padding="0,0,4,0"
|
||||
|
||||
@@ -126,7 +126,7 @@ public sealed partial class CommandBar : UserControl,
|
||||
|
||||
private void SettingsIcon_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>(new());
|
||||
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>();
|
||||
}
|
||||
|
||||
private void MoreCommandsButton_Clicked(object sender, RoutedEventArgs e)
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="Microsoft.CmdPal.UI.Controls.ScrollContainer"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
|
||||
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<Style x:Key="ScrollButtonStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="{ThemeResource FlipViewNextPreviousButtonBackground}" />
|
||||
<Setter Property="BackgroundSizing" Value="InnerBorderEdge" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource ButtonForeground}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource FlipViewNextPreviousButtonBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
|
||||
<Setter Property="FontWeight" Value="Normal" />
|
||||
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
|
||||
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
|
||||
<Setter Property="FocusVisualMargin" Value="-3" />
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<ContentPresenter
|
||||
x:Name="ContentPresenter"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
AnimatedIcon.State="Normal"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Background="{TemplateBinding Background}"
|
||||
BackgroundSizing="{TemplateBinding BackgroundSizing}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
ContentTransitions="{TemplateBinding ContentTransitions}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<ContentPresenter.BackgroundTransition>
|
||||
<BrushTransition Duration="0:0:0.083" />
|
||||
</ContentPresenter.BackgroundTransition>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="PointerOver">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousButtonBackgroundPointerOver}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousButtonBorderBrushPointerOver}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousArrowForegroundPointerOver}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="PointerOver" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Pressed">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousButtonBackgroundPressed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousButtonBorderBrushPressed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousArrowForegroundPressed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="Pressed" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Disabled">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonBackgroundDisabled}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonBorderBrushDisabled}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonForegroundDisabled}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<VisualState.Setters>
|
||||
<!-- DisabledVisual Should be handled by the control, not the animated icon. -->
|
||||
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="Normal" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</ContentPresenter>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
<Grid x:Name="RootGrid">
|
||||
<ScrollViewer
|
||||
x:Name="scroller"
|
||||
HorizontalScrollBarVisibility="Hidden"
|
||||
HorizontalScrollMode="Enabled"
|
||||
SizeChanged="Scroller_SizeChanged"
|
||||
VerticalScrollBarVisibility="Hidden"
|
||||
VerticalScrollMode="Disabled"
|
||||
ViewChanging="Scroller_ViewChanging">
|
||||
<Grid x:Name="ContentGrid">
|
||||
<ContentPresenter Content="{x:Bind Source, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
<Button
|
||||
x:Name="ScrollBackBtn"
|
||||
Margin="8,0,0,0"
|
||||
Padding="2,8,2,8"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="Scroll left"
|
||||
Click="ScrollBackBtn_Click"
|
||||
Style="{StaticResource ScrollButtonStyle}"
|
||||
ToolTipService.ToolTip="Scroll left"
|
||||
Visibility="Collapsed">
|
||||
<FontIcon
|
||||
x:Name="ScrollBackIcon"
|
||||
FontSize="{ThemeResource FlipViewButtonFontSize}"
|
||||
Glyph="" />
|
||||
</Button>
|
||||
<Button
|
||||
x:Name="ScrollForwardBtn"
|
||||
Margin="0,0,8,0"
|
||||
Padding="2,8,2,8"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="Scroll right"
|
||||
Click="ScrollForwardBtn_Click"
|
||||
Style="{StaticResource ScrollButtonStyle}"
|
||||
ToolTipService.ToolTip="Scroll right">
|
||||
<FontIcon
|
||||
x:Name="ScrollForwardIcon"
|
||||
FontSize="{ThemeResource FlipViewButtonFontSize}"
|
||||
Glyph="" />
|
||||
</Button>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="OrientationStates">
|
||||
<VisualState x:Name="HorizontalState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="scroller.HorizontalScrollBarVisibility" Value="Hidden" />
|
||||
<Setter Target="scroller.HorizontalScrollMode" Value="Enabled" />
|
||||
<Setter Target="scroller.VerticalScrollBarVisibility" Value="Hidden" />
|
||||
<Setter Target="scroller.VerticalScrollMode" Value="Disabled" />
|
||||
<Setter Target="ScrollBackBtn.Padding" Value="4,12,4,12" />
|
||||
<Setter Target="ScrollBackBtn.Margin" Value="8,0,0,0" />
|
||||
<Setter Target="ScrollBackBtn.HorizontalAlignment" Value="Left" />
|
||||
<Setter Target="ScrollBackBtn.VerticalAlignment" Value="Center" />
|
||||
<Setter Target="ScrollBackBtn.(AutomationProperties.Name)" Value="Scroll left" />
|
||||
<Setter Target="ScrollBackBtn.(ToolTipService.ToolTip)" Value="Scroll left" />
|
||||
<Setter Target="ScrollBackIcon.Glyph" Value="" />
|
||||
<Setter Target="ScrollForwardBtn.Padding" Value="4,12,4,12" />
|
||||
<Setter Target="ScrollForwardBtn.Margin" Value="0,0,8,0" />
|
||||
<Setter Target="ScrollForwardBtn.HorizontalAlignment" Value="Right" />
|
||||
<Setter Target="ScrollForwardBtn.VerticalAlignment" Value="Center" />
|
||||
<Setter Target="ScrollForwardBtn.(AutomationProperties.Name)" Value="Scroll right" />
|
||||
<Setter Target="ScrollForwardBtn.(ToolTipService.ToolTip)" Value="Scroll right" />
|
||||
<Setter Target="ScrollForwardIcon.Glyph" Value="" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="VerticalState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="scroller.HorizontalScrollBarVisibility" Value="Hidden" />
|
||||
<Setter Target="scroller.HorizontalScrollMode" Value="Disabled" />
|
||||
<Setter Target="scroller.VerticalScrollBarVisibility" Value="Hidden" />
|
||||
<Setter Target="scroller.VerticalScrollMode" Value="Enabled" />
|
||||
<Setter Target="ScrollBackBtn.Padding" Value="12,4,12,4" />
|
||||
<Setter Target="ScrollBackBtn.Margin" Value="0,8,0,0" />
|
||||
<Setter Target="ScrollBackBtn.HorizontalAlignment" Value="Center" />
|
||||
<Setter Target="ScrollBackBtn.VerticalAlignment" Value="Top" />
|
||||
<Setter Target="ScrollBackBtn.(AutomationProperties.Name)" Value="Scroll up" />
|
||||
<Setter Target="ScrollBackBtn.(ToolTipService.ToolTip)" Value="Scroll up" />
|
||||
<Setter Target="ScrollBackIcon.Glyph" Value="" />
|
||||
<Setter Target="ScrollForwardBtn.Padding" Value="12,4,12,4" />
|
||||
<Setter Target="ScrollForwardBtn.Margin" Value="0,0,0,8" />
|
||||
<Setter Target="ScrollForwardBtn.HorizontalAlignment" Value="Center" />
|
||||
<Setter Target="ScrollForwardBtn.VerticalAlignment" Value="Bottom" />
|
||||
<Setter Target="ScrollForwardBtn.(AutomationProperties.Name)" Value="Scroll down" />
|
||||
<Setter Target="ScrollForwardBtn.(ToolTipService.ToolTip)" Value="Scroll down" />
|
||||
<Setter Target="ScrollForwardIcon.Glyph" Value="" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -1,185 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Windows.Foundation;
|
||||
using Windows.Foundation.Collections;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
public sealed partial class ScrollContainer : UserControl
|
||||
{
|
||||
public enum ScrollContentAlignment
|
||||
{
|
||||
Start,
|
||||
End,
|
||||
}
|
||||
|
||||
public ScrollContainer()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += ScrollContainer_Loaded;
|
||||
}
|
||||
|
||||
private void ScrollContainer_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
UpdateOrientationState();
|
||||
}
|
||||
|
||||
public object Source
|
||||
{
|
||||
get => (object)GetValue(SourceProperty);
|
||||
set => SetValue(SourceProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty SourceProperty =
|
||||
DependencyProperty.Register(nameof(Source), typeof(object), typeof(ScrollContainer), new PropertyMetadata(null));
|
||||
|
||||
public Orientation Orientation
|
||||
{
|
||||
get => (Orientation)GetValue(OrientationProperty);
|
||||
set => SetValue(OrientationProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty OrientationProperty =
|
||||
DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(ScrollContainer), new PropertyMetadata(Orientation.Horizontal, OnOrientationChanged));
|
||||
|
||||
public ScrollContentAlignment ContentAlignment
|
||||
{
|
||||
get => (ScrollContentAlignment)GetValue(ContentAlignmentProperty);
|
||||
set => SetValue(ContentAlignmentProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ContentAlignmentProperty =
|
||||
DependencyProperty.Register(nameof(ContentAlignment), typeof(ScrollContentAlignment), typeof(ScrollContainer), new PropertyMetadata(ScrollContentAlignment.Start, OnContentAlignmentChanged));
|
||||
|
||||
private static void OnContentAlignmentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is ScrollContainer control)
|
||||
{
|
||||
control.ScrollToAlignment();
|
||||
}
|
||||
}
|
||||
|
||||
private void ScrollToAlignment()
|
||||
{
|
||||
// Reset button visibility
|
||||
ScrollBackBtn.Visibility = Visibility.Collapsed;
|
||||
ScrollForwardBtn.Visibility = Visibility.Collapsed;
|
||||
|
||||
if (ContentAlignment == ScrollContentAlignment.End)
|
||||
{
|
||||
// Scroll to the end
|
||||
if (Orientation == Orientation.Horizontal)
|
||||
{
|
||||
scroller.ChangeView(scroller.ScrollableWidth, null, null, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
scroller.ChangeView(null, scroller.ScrollableHeight, null, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Scroll to the beginning
|
||||
scroller.ChangeView(0, 0, null, true);
|
||||
}
|
||||
|
||||
// Defer visibility update until after layout
|
||||
void OnLayoutUpdated(object? sender, object args)
|
||||
{
|
||||
scroller.LayoutUpdated -= OnLayoutUpdated;
|
||||
UpdateScrollButtonsVisibility();
|
||||
}
|
||||
|
||||
scroller.LayoutUpdated += OnLayoutUpdated;
|
||||
}
|
||||
|
||||
private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is ScrollContainer control)
|
||||
{
|
||||
control.UpdateOrientationState();
|
||||
control.ScrollToAlignment();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateOrientationState()
|
||||
{
|
||||
var stateName = Orientation == Orientation.Horizontal ? "HorizontalState" : "VerticalState";
|
||||
VisualStateManager.GoToState(this, stateName, true);
|
||||
}
|
||||
|
||||
private void Scroller_ViewChanging(object sender, ScrollViewerViewChangingEventArgs e)
|
||||
{
|
||||
UpdateScrollButtonsVisibility(e.FinalView.HorizontalOffset, e.FinalView.VerticalOffset);
|
||||
}
|
||||
|
||||
private void ScrollBackBtn_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Orientation == Orientation.Horizontal)
|
||||
{
|
||||
scroller.ChangeView(scroller.HorizontalOffset - scroller.ViewportWidth, null, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
scroller.ChangeView(null, scroller.VerticalOffset - scroller.ViewportHeight, null);
|
||||
}
|
||||
|
||||
// Manually focus to ScrollForwardBtn since this button disappears after scrolling to the end.
|
||||
ScrollForwardBtn.Focus(FocusState.Programmatic);
|
||||
}
|
||||
|
||||
private void ScrollForwardBtn_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Orientation == Orientation.Horizontal)
|
||||
{
|
||||
scroller.ChangeView(scroller.HorizontalOffset + scroller.ViewportWidth, null, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
scroller.ChangeView(null, scroller.VerticalOffset + scroller.ViewportHeight, null);
|
||||
}
|
||||
|
||||
// Manually focus to ScrollBackBtn since this button disappears after scrolling to the end.
|
||||
ScrollBackBtn.Focus(FocusState.Programmatic);
|
||||
}
|
||||
|
||||
private void Scroller_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
UpdateScrollButtonsVisibility();
|
||||
}
|
||||
|
||||
private void UpdateScrollButtonsVisibility(double? horizontalOffset = null, double? verticalOffset = null)
|
||||
{
|
||||
var hOffset = horizontalOffset ?? scroller.HorizontalOffset;
|
||||
var vOffset = verticalOffset ?? scroller.VerticalOffset;
|
||||
|
||||
if (Orientation == Orientation.Horizontal)
|
||||
{
|
||||
ScrollBackBtn.Visibility = hOffset > 1 ? Visibility.Visible : Visibility.Collapsed;
|
||||
ScrollForwardBtn.Visibility = scroller.ScrollableWidth > 0 && hOffset < scroller.ScrollableWidth - 1
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
ScrollBackBtn.Visibility = vOffset > 1 ? Visibility.Visible : Visibility.Collapsed;
|
||||
ScrollForwardBtn.Visibility = scroller.ScrollableHeight > 0 && vOffset < scroller.ScrollableHeight - 1
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,273 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="Microsoft.CmdPal.UI.Dock.DockControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:coreVm="using:Microsoft.CmdPal.Core.ViewModels"
|
||||
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:dockVm="using:Microsoft.CmdPal.UI.ViewModels.Dock"
|
||||
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Dock"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<StackLayout
|
||||
x:Key="ItemsOrientation"
|
||||
Orientation="{x:Bind ItemsOrientation, Mode=OneWay}"
|
||||
Spacing="4" />
|
||||
|
||||
<Style x:Key="ResizingIconStyle" TargetType="cpcontrols:IconBox">
|
||||
<Setter Property="Height" Value="{x:Bind IconSize, Mode=OneWay}" />
|
||||
<Setter Property="MaxWidth" Value="{x:Bind IconSize, Mode=OneWay}" />
|
||||
<Setter Property="MinWidth" Value="{x:Bind IconMinWidth, Mode=OneWay}" />
|
||||
</Style>
|
||||
<Style x:Key="ResizingTitleTextBlock" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="{x:Bind TitleTextFontSize, Mode=OneWay}" />
|
||||
<Setter Property="MaxWidth" Value="{x:Bind TitleTextMaxWidth, Mode=OneWay}" />
|
||||
</Style>
|
||||
|
||||
<local:IconInfoVisibilityConverter x:Key="IconInfoVisibilityConverter" />
|
||||
<local:BandAlignmentConverter
|
||||
x:Key="BandAlignmentConverter"
|
||||
x:Name="BandAlignmentConverter"
|
||||
Control="{x:Bind}" />
|
||||
|
||||
<DataTemplate x:Key="DeskbandTemplate" x:DataType="dockVm:DockItemViewModel">
|
||||
<Button
|
||||
VerticalAlignment="Stretch"
|
||||
DataContext="{x:Bind}"
|
||||
RightTapped="BandItem_RightTapped"
|
||||
Style="{StaticResource TaskBarButtonStyle}"
|
||||
Tapped="BandItem_Tapped"
|
||||
ToolTipService.ToolTip="{x:Bind Tooltip, Mode=OneWay}">
|
||||
|
||||
<Grid AutomationProperties.Name="{x:Bind Title, Mode=OneWay}" Background="Transparent">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Vertical"
|
||||
Visibility="{x:Bind Icon, Converter={StaticResource IconInfoVisibilityConverter}, Mode=OneWay}">
|
||||
<cpcontrols:IconBox
|
||||
x:Name="IconBorder"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}"
|
||||
Style="{StaticResource ResizingIconStyle}" />
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
Margin="8,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{x:Bind HasText, Mode=OneWay}">
|
||||
<TextBlock
|
||||
x:Name="TitleTextBlock"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="12"
|
||||
Style="{StaticResource ResizingTitleTextBlock}"
|
||||
Text="{x:Bind Title, Mode=OneWay}"
|
||||
TextAlignment="Left"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap" />
|
||||
<TextBlock
|
||||
x:Name="SubTitleTextBlock"
|
||||
MaxWidth="100"
|
||||
Margin="0,-4,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="10"
|
||||
Foreground="{ThemeResource TextFillColorTertiary}"
|
||||
Text="{x:Bind Subtitle, Mode=OneWay}"
|
||||
TextAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap"
|
||||
Visibility="{x:Bind Subtitle, Mode=OneWay, Converter={StaticResource StringNotEmptyToVisibilityConverter}}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="DockBandTemplate" x:DataType="dockVm:DockBandViewModel">
|
||||
<ItemsRepeater
|
||||
x:Name="BandItemsRepeater"
|
||||
HorizontalAlignment="{x:Bind Items, Converter={StaticResource BandAlignmentConverter}, Mode=OneWay}"
|
||||
ItemTemplate="{StaticResource DeskbandTemplate}"
|
||||
ItemsSource="{x:Bind Items, Mode=OneWay}"
|
||||
Layout="{StaticResource ItemsOrientation}">
|
||||
<ItemsRepeater.Transitions>
|
||||
<TransitionCollection />
|
||||
</ItemsRepeater.Transitions>
|
||||
</ItemsRepeater>
|
||||
</DataTemplate>
|
||||
|
||||
<Style
|
||||
x:Name="ContextMenuFlyoutStyle"
|
||||
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
|
||||
TargetType="FlyoutPresenter">
|
||||
<Style.Setters>
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Background" Value="{ThemeResource DesktopAcrylicTransparentBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
|
||||
<!-- Backdrop requires ShouldConstrainToRootBounds="False" -->
|
||||
<Flyout
|
||||
x:Name="ContextMenuFlyout"
|
||||
FlyoutPresenterStyle="{StaticResource ContextMenuFlyoutStyle}"
|
||||
Opened="ContextMenuFlyout_Opened"
|
||||
ShouldConstrainToRootBounds="False"
|
||||
SystemBackdrop="{ThemeResource AcrylicBackgroundFillColorDefaultBackdrop}">
|
||||
<cpcontrols:ContextMenu x:Name="ContextControl" />
|
||||
</Flyout>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid
|
||||
x:Name="RootGrid"
|
||||
BorderBrush="{ThemeResource SurfaceStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,0,0,1"
|
||||
RightTapped="RootGrid_RightTapped">
|
||||
<Grid x:Name="ContentGrid" Padding="0,0,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="2*" />
|
||||
<ColumnDefinition x:Name="EndColumn" Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="2*" />
|
||||
<RowDefinition x:Name="EndRow" Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<cpcontrols:ScrollContainer
|
||||
x:Name="StartItemsView"
|
||||
Grid.Column="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<cpcontrols:ScrollContainer.Source>
|
||||
<ItemsRepeater
|
||||
x:Name="StartItemsRepeater"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemTemplate="{StaticResource DockBandTemplate}"
|
||||
ItemsSource="{x:Bind ViewModel.StartItems, Mode=OneWay}"
|
||||
Layout="{StaticResource ItemsOrientation}">
|
||||
<ItemsRepeater.Transitions>
|
||||
<TransitionCollection />
|
||||
</ItemsRepeater.Transitions>
|
||||
</ItemsRepeater>
|
||||
</cpcontrols:ScrollContainer.Source>
|
||||
</cpcontrols:ScrollContainer>
|
||||
|
||||
<cpcontrols:ScrollContainer
|
||||
x:Name="EndItemsView"
|
||||
Grid.Column="1"
|
||||
ContentAlignment="End">
|
||||
<cpcontrols:ScrollContainer.Source>
|
||||
<ItemsRepeater
|
||||
x:Name="EndItemsRepeater"
|
||||
ItemTemplate="{StaticResource DockBandTemplate}"
|
||||
ItemsSource="{x:Bind ViewModel.EndItems, Mode=OneWay}"
|
||||
Layout="{StaticResource ItemsOrientation}">
|
||||
<ItemsRepeater.Transitions>
|
||||
<TransitionCollection />
|
||||
</ItemsRepeater.Transitions>
|
||||
</ItemsRepeater>
|
||||
</cpcontrols:ScrollContainer.Source>
|
||||
</cpcontrols:ScrollContainer>
|
||||
</Grid>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="DockOrientation">
|
||||
<VisualState x:Name="DockOnTop">
|
||||
<VisualState.StateTriggers>
|
||||
<ui:IsEqualStateTrigger Value="{x:Bind DockSide, Mode=OneWay}" To="Top" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="StartItemsView.(Grid.Row)" Value="0" />
|
||||
<Setter Target="StartItemsView.(Grid.Column)" Value="0" />
|
||||
<Setter Target="StartItemsView.(Grid.RowSpan)" Value="3" />
|
||||
<Setter Target="StartItemsView.(Grid.ColumnSpan)" Value="1" />
|
||||
<Setter Target="EndItemsView.(Grid.Row)" Value="0" />
|
||||
<Setter Target="EndItemsView.(Grid.Column)" Value="3" />
|
||||
<Setter Target="EndItemsView.(Grid.RowSpan)" Value="3" />
|
||||
<Setter Target="EndItemsView.(Grid.ColumnSpan)" Value="1" />
|
||||
<Setter Target="EndItemsView.HorizontalAlignment" Value="Right" />
|
||||
<Setter Target="ContentGrid.Margin" Value="8,0,8,0" />
|
||||
<Setter Target="RootGrid.BorderThickness" Value="0,0,0,1" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="DockOnBottom">
|
||||
<VisualState.StateTriggers>
|
||||
<ui:IsEqualStateTrigger Value="{x:Bind DockSide, Mode=OneWay}" To="Bottom" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="StartItemsView.(Grid.Row)" Value="0" />
|
||||
<Setter Target="StartItemsView.(Grid.Column)" Value="0" />
|
||||
<Setter Target="StartItemsView.(Grid.RowSpan)" Value="3" />
|
||||
<Setter Target="StartItemsView.(Grid.ColumnSpan)" Value="1" />
|
||||
<Setter Target="EndItemsView.(Grid.Row)" Value="0" />
|
||||
<Setter Target="EndItemsView.(Grid.Column)" Value="3" />
|
||||
<Setter Target="EndItemsView.(Grid.RowSpan)" Value="3" />
|
||||
<Setter Target="EndItemsView.(Grid.ColumnSpan)" Value="1" />
|
||||
<Setter Target="EndItemsView.HorizontalAlignment" Value="Right" />
|
||||
<Setter Target="ContentGrid.Margin" Value="8,0,8,0" />
|
||||
<Setter Target="RootGrid.BorderThickness" Value="0,1,0,0" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="DockOnLeft">
|
||||
<VisualState.StateTriggers>
|
||||
<ui:IsEqualStateTrigger Value="{x:Bind DockSide, Mode=OneWay}" To="Left" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="StartItemsView.(Grid.Row)" Value="0" />
|
||||
<Setter Target="StartItemsView.(Grid.Column)" Value="0" />
|
||||
<Setter Target="StartItemsView.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="StartItemsView.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="StartItemsView.Orientation" Value="Vertical" />
|
||||
<Setter Target="EndItemsView.Orientation" Value="Vertical" />
|
||||
<Setter Target="EndItemsView.(Grid.Row)" Value="3" />
|
||||
<Setter Target="EndItemsView.(Grid.Column)" Value="0" />
|
||||
<Setter Target="EndItemsView.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="EndItemsView.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="EndItemsView.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="ContentGrid.Padding" Value="4,8,4,8" />
|
||||
<Setter Target="RootGrid.BorderThickness" Value="0,0,1,0" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="DockOnRight">
|
||||
<VisualState.StateTriggers>
|
||||
<ui:IsEqualStateTrigger Value="{x:Bind DockSide, Mode=OneWay}" To="Right" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="StartItemsView.(Grid.Row)" Value="0" />
|
||||
<Setter Target="StartItemsView.(Grid.Column)" Value="0" />
|
||||
<Setter Target="StartItemsView.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="StartItemsView.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="StartItemsView.Orientation" Value="Vertical" />
|
||||
<Setter Target="EndItemsView.Orientation" Value="Vertical" />
|
||||
<Setter Target="EndItemsView.(Grid.Row)" Value="3" />
|
||||
<Setter Target="EndItemsView.(Grid.Column)" Value="0" />
|
||||
<Setter Target="EndItemsView.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="EndItemsView.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="EndItemsView.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="ContentGrid.Padding" Value="4,8,4,8" />
|
||||
<Setter Target="RootGrid.BorderThickness" Value="1,0,0,0" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -1,281 +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 System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Dock;
|
||||
|
||||
#pragma warning disable SA1402 // File may only contain a single type
|
||||
|
||||
public sealed partial class DockControl : UserControl, INotifyPropertyChanged, IRecipient<CloseContextMenuMessage>
|
||||
{
|
||||
private DockViewModel _viewModel;
|
||||
|
||||
internal DockViewModel ViewModel => _viewModel;
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public Orientation ItemsOrientation
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (field != value)
|
||||
{
|
||||
field = value;
|
||||
PropertyChanged?.Invoke(this, new(nameof(ItemsOrientation)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DockSide DockSide
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (field != value)
|
||||
{
|
||||
field = value;
|
||||
PropertyChanged?.Invoke(this, new(nameof(DockSide)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public double IconSize
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (field != value)
|
||||
{
|
||||
field = value;
|
||||
PropertyChanged?.Invoke(this, new(nameof(IconSize)));
|
||||
PropertyChanged?.Invoke(this, new(nameof(IconMinWidth)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
= 16.0;
|
||||
|
||||
public double IconMinWidth => IconSize / 2;
|
||||
|
||||
public double TitleTextMaxWidth
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (field != value)
|
||||
{
|
||||
field = value;
|
||||
PropertyChanged?.Invoke(this, new(nameof(TitleTextMaxWidth)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
= 100;
|
||||
|
||||
public double TitleTextFontSize
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (field != value)
|
||||
{
|
||||
field = value;
|
||||
PropertyChanged?.Invoke(this, new(nameof(TitleTextFontSize)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
= 12;
|
||||
|
||||
internal DockControl(DockViewModel viewModel)
|
||||
{
|
||||
_viewModel = viewModel;
|
||||
InitializeComponent();
|
||||
WeakReferenceMessenger.Default.Register<CloseContextMenuMessage>(this);
|
||||
}
|
||||
|
||||
internal void UpdateSettings(DockSettings settings)
|
||||
{
|
||||
DockSide = settings.Side;
|
||||
|
||||
var isHorizontal = settings.Side == DockSide.Top || settings.Side == DockSide.Bottom;
|
||||
|
||||
ItemsOrientation = isHorizontal ? Orientation.Horizontal : Orientation.Vertical;
|
||||
|
||||
IconSize = DockSettingsToViews.IconSizeForSize(settings.DockIconsSize);
|
||||
TitleTextFontSize = DockSettingsToViews.TitleTextFontSizeForSize(settings.DockSize);
|
||||
TitleTextMaxWidth = DockSettingsToViews.TitleTextMaxWidthForSize(settings.DockSize);
|
||||
|
||||
if (settings.Backdrop == DockBackdrop.Transparent)
|
||||
{
|
||||
RootGrid.BorderBrush = new SolidColorBrush(Colors.Transparent);
|
||||
}
|
||||
}
|
||||
|
||||
private void BandItem_Tapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e)
|
||||
{
|
||||
var pos = e.GetPosition(null);
|
||||
var button = sender as Button;
|
||||
var item = button?.DataContext as DockItemViewModel;
|
||||
|
||||
if (item is not null)
|
||||
{
|
||||
// Use the center of the button as the point to open at. This is
|
||||
// more reliable than using the tap position. This allows multiple
|
||||
// clicks anywhere in the button to open the palette in a consistent
|
||||
// location.
|
||||
var buttonPos = button!.TransformToVisual(null).TransformPoint(new Point(0, 0));
|
||||
var buttonCenter = new Point(
|
||||
buttonPos.X + (button.ActualWidth / 2),
|
||||
buttonPos.Y + (button.ActualHeight / 2));
|
||||
|
||||
InvokeItem(item, buttonCenter);
|
||||
}
|
||||
}
|
||||
|
||||
private void BandItem_RightTapped(object sender, Microsoft.UI.Xaml.Input.RightTappedRoutedEventArgs e)
|
||||
{
|
||||
var pos = e.GetPosition(null);
|
||||
var button = sender as Button;
|
||||
var item = button?.DataContext as DockItemViewModel;
|
||||
if (item is not null)
|
||||
{
|
||||
if (item.HasMoreCommands)
|
||||
{
|
||||
ContextControl.ViewModel.SelectedItem = item;
|
||||
ContextMenuFlyout.ShowAt(
|
||||
button,
|
||||
new FlyoutShowOptions()
|
||||
{
|
||||
ShowMode = FlyoutShowMode.Standard,
|
||||
Placement = FlyoutPlacementMode.TopEdgeAlignedRight,
|
||||
});
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void InvokeItem(DockItemViewModel item, global::Windows.Foundation.Point pos)
|
||||
{
|
||||
var command = item.Command;
|
||||
try
|
||||
{
|
||||
var isPage = command.Model.Unsafe is not IInvokableCommand invokable;
|
||||
if (isPage)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<RequestShowPaletteAtMessage>(new(pos));
|
||||
}
|
||||
|
||||
PerformCommandMessage m = new(command.Model);
|
||||
m.WithAnimation = false;
|
||||
m.TransientPage = true;
|
||||
WeakReferenceMessenger.Default.Send(m);
|
||||
}
|
||||
catch (COMException e)
|
||||
{
|
||||
Logger.LogError("Error invoking dock command", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void ContextMenuFlyout_Opened(object sender, object e)
|
||||
{
|
||||
// We need to wait until our flyout is opened to try and toss focus
|
||||
// at its search box. The control isn't in the UI tree before that
|
||||
ContextControl.FocusSearchBox();
|
||||
}
|
||||
|
||||
public void Receive(CloseContextMenuMessage message)
|
||||
{
|
||||
if (ContextMenuFlyout.IsOpen)
|
||||
{
|
||||
ContextMenuFlyout.Hide();
|
||||
}
|
||||
}
|
||||
|
||||
private void RootGrid_RightTapped(object sender, Microsoft.UI.Xaml.Input.RightTappedRoutedEventArgs e)
|
||||
{
|
||||
var pos = e.GetPosition(null);
|
||||
var item = this.ViewModel.GetContextMenuForDock();
|
||||
if (item.HasMoreCommands)
|
||||
{
|
||||
ContextControl.ViewModel.SelectedItem = item;
|
||||
ContextMenuFlyout.ShowAt(
|
||||
this.RootGrid,
|
||||
new FlyoutShowOptions()
|
||||
{
|
||||
ShowMode = FlyoutShowMode.Standard,
|
||||
Placement = FlyoutPlacementMode.TopEdgeAlignedRight,
|
||||
Position = pos,
|
||||
});
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
internal HorizontalAlignment GetBandAlignment(ObservableCollection<DockItemViewModel> items)
|
||||
{
|
||||
if (DockSide == DockSide.Top || DockSide == DockSide.Bottom)
|
||||
{
|
||||
return HorizontalAlignment.Center;
|
||||
}
|
||||
|
||||
var requestedTheme = ActualTheme;
|
||||
var isLight = requestedTheme == Microsoft.UI.Xaml.ElementTheme.Light;
|
||||
|
||||
// Check if any of the items have both an icon and a label.
|
||||
//
|
||||
// If so, left align so that the icons don't wobble if the text
|
||||
// changes.
|
||||
//
|
||||
// Otherwise, center align.
|
||||
foreach (var item in items)
|
||||
{
|
||||
var showText = item.ShowLabel && item.HasText;
|
||||
var showIcon = item.Icon is not null && item.Icon.HasIcon(isLight);
|
||||
if (showText && showIcon)
|
||||
{
|
||||
return HorizontalAlignment.Left;
|
||||
}
|
||||
}
|
||||
|
||||
return HorizontalAlignment.Center;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed partial class BandAlignmentConverter : Microsoft.UI.Xaml.Data.IValueConverter
|
||||
{
|
||||
public DockControl? Control { get; set; }
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is ObservableCollection<DockItemViewModel> items && Control is not null)
|
||||
{
|
||||
return Control.GetBandAlignment(items);
|
||||
}
|
||||
|
||||
return HorizontalAlignment.Center;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning restore SA1402 // File may only contain a single type
|
||||
@@ -1,86 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.Win32;
|
||||
using WinUIEx;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Dock;
|
||||
|
||||
internal static class DockSettingsToViews
|
||||
{
|
||||
public static double WidthForSize(DockSize size)
|
||||
{
|
||||
return size switch
|
||||
{
|
||||
DockSize.Small => 128,
|
||||
DockSize.Medium => 192,
|
||||
DockSize.Large => 256,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static double TitleTextFontSizeForSize(DockSize size)
|
||||
{
|
||||
return size switch
|
||||
{
|
||||
DockSize.Small => 12,
|
||||
DockSize.Medium => 16,
|
||||
DockSize.Large => 20,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static double TitleTextMaxWidthForSize(DockSize size)
|
||||
{
|
||||
return WidthForSize(size) - TitleTextFontSizeForSize(size);
|
||||
}
|
||||
|
||||
public static double HeightForSize(DockSize size)
|
||||
{
|
||||
return size switch
|
||||
{
|
||||
DockSize.Small => 40,
|
||||
DockSize.Medium => 54,
|
||||
DockSize.Large => 76,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static double IconSizeForSize(DockSize size)
|
||||
{
|
||||
return size switch
|
||||
{
|
||||
DockSize.Small => 32 / 2,
|
||||
DockSize.Medium => 54 / 2,
|
||||
DockSize.Large => 76 / 2,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static Microsoft.UI.Xaml.Media.SystemBackdrop? GetSystemBackdrop(DockBackdrop backdrop)
|
||||
{
|
||||
return backdrop switch
|
||||
{
|
||||
DockBackdrop.Mica => new MicaBackdrop(),
|
||||
DockBackdrop.Transparent => new TransparentTintBackdrop(),
|
||||
DockBackdrop.Acrylic => null, // new DesktopAcrylicBackdrop(),
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static uint GetAppBarEdge(DockSide side)
|
||||
{
|
||||
return side switch
|
||||
{
|
||||
DockSide.Left => PInvoke.ABE_LEFT,
|
||||
DockSide.Top => PInvoke.ABE_TOP,
|
||||
DockSide.Right => PInvoke.ABE_RIGHT,
|
||||
DockSide.Bottom => PInvoke.ABE_BOTTOM,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
}
|
||||
#pragma warning restore SA1402 // File may only contain a single type
|
||||
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<winuiex:WindowEx
|
||||
x:Class="Microsoft.CmdPal.UI.Dock.DockWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Dock"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:vm="using:Microsoft.CmdPal.UI.ViewModels"
|
||||
xmlns:winuiex="using:WinUIEx"
|
||||
Title="PowerDock"
|
||||
Closed="DockWindow_Closed"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid
|
||||
x:Name="Root"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch" />
|
||||
</winuiex:WindowEx>
|
||||
@@ -1,711 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Composition.SystemBackdrops;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.Foundation;
|
||||
using Windows.UI;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.Graphics.Dwm;
|
||||
using Windows.Win32.UI.Accessibility;
|
||||
using Windows.Win32.UI.Shell;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
using WinRT;
|
||||
using WinRT.Interop;
|
||||
using WinUIEx;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Dock;
|
||||
|
||||
#pragma warning disable SA1402 // File may only contain a single type
|
||||
|
||||
public sealed partial class DockWindow : WindowEx,
|
||||
IRecipient<BringToTopMessage>,
|
||||
IRecipient<RequestShowPaletteAtMessage>,
|
||||
IRecipient<QuitMessage>,
|
||||
IDisposable
|
||||
{
|
||||
#pragma warning disable SA1306 // Field names should begin with lower-case letter
|
||||
#pragma warning disable SA1310 // Field names should not contain underscore
|
||||
private readonly uint WM_TASKBAR_RESTART;
|
||||
#pragma warning restore SA1310 // Field names should not contain underscore
|
||||
#pragma warning restore SA1306 // Field names should begin with lower-case letter
|
||||
|
||||
private HWND _hwnd = HWND.Null;
|
||||
private APPBARDATA _appBarData;
|
||||
private uint _callbackMessageId;
|
||||
|
||||
private DockSettings _settings;
|
||||
private DockViewModel viewModel;
|
||||
private DockControl _dock;
|
||||
private DesktopAcrylicController? _acrylicController;
|
||||
private SystemBackdropConfiguration? _configurationSource;
|
||||
private DockSize _lastSize;
|
||||
|
||||
// Store the original WndProc
|
||||
private WNDPROC? _originalWndProc;
|
||||
private WNDPROC? _customWndProc;
|
||||
|
||||
// internal Settings CurrentSettings => _settings;
|
||||
public DockWindow()
|
||||
{
|
||||
var serviceProvider = App.Current.Services;
|
||||
var mainSettings = serviceProvider.GetService<SettingsModel>()!;
|
||||
mainSettings.SettingsChanged += SettingsChangedHandler;
|
||||
_settings = mainSettings.DockSettings;
|
||||
_lastSize = _settings.DockSize;
|
||||
|
||||
viewModel = serviceProvider.GetService<DockViewModel>()!;
|
||||
_dock = new DockControl(viewModel);
|
||||
|
||||
InitializeComponent();
|
||||
Root.Children.Add(_dock);
|
||||
ExtendsContentIntoTitleBar = true;
|
||||
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
|
||||
AppWindow.IsShownInSwitchers = false;
|
||||
if (AppWindow.Presenter is OverlappedPresenter overlappedPresenter)
|
||||
{
|
||||
overlappedPresenter.SetBorderAndTitleBar(false, false);
|
||||
overlappedPresenter.IsResizable = false;
|
||||
}
|
||||
|
||||
this.Activated += DockWindow_Activated;
|
||||
|
||||
WeakReferenceMessenger.Default.Register<BringToTopMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<RequestShowPaletteAtMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<QuitMessage>(this);
|
||||
|
||||
_hwnd = GetWindowHandle(this);
|
||||
|
||||
// Subclass the window to intercept messages
|
||||
//
|
||||
// Set up custom window procedure to listen for display changes
|
||||
// LOAD BEARING: If you don't stick the pointer to HotKeyPrc into a
|
||||
// member (and instead like, use a local), then the pointer we marshal
|
||||
// into the WindowLongPtr will be useless after we leave this function,
|
||||
// and our **WindProc will explode**.
|
||||
_customWndProc = CustomWndProc;
|
||||
|
||||
_callbackMessageId = PInvoke.RegisterWindowMessage($"CmdPal_ABM_{_hwnd}");
|
||||
|
||||
// TaskbarCreated is the message that's broadcast when explorer.exe
|
||||
// restarts. We need to know when that happens to be able to bring our
|
||||
// appbar back
|
||||
// And this apparently happens on lock screens / hibernates, too
|
||||
WM_TASKBAR_RESTART = PInvoke.RegisterWindowMessage("TaskbarCreated");
|
||||
|
||||
var hotKeyPrcPointer = Marshal.GetFunctionPointerForDelegate(_customWndProc);
|
||||
_originalWndProc = Marshal.GetDelegateForFunctionPointer<WNDPROC>(PInvoke.SetWindowLongPtr(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer));
|
||||
|
||||
// Disable minimize and maximize box
|
||||
var style = (WINDOW_STYLE)PInvoke.GetWindowLong(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE);
|
||||
style &= ~WINDOW_STYLE.WS_MINIMIZEBOX; // Remove WS_MINIMIZEBOX
|
||||
style &= ~WINDOW_STYLE.WS_MAXIMIZEBOX; // Remove WS_MAXIMIZEBOX
|
||||
_ = PInvoke.SetWindowLong(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE, (int)style);
|
||||
|
||||
ShowDesktop.AddHook(this);
|
||||
UpdateSettings();
|
||||
}
|
||||
|
||||
private void SettingsChangedHandler(SettingsModel sender, object? args)
|
||||
{
|
||||
_settings = sender.DockSettings;
|
||||
UpdateSettings();
|
||||
}
|
||||
|
||||
private void DockWindow_Activated(object sender, WindowActivatedEventArgs args)
|
||||
{
|
||||
// These are used for removing the very subtle shadow/border that we get from Windows 11
|
||||
HwndExtensions.ToggleWindowStyle(_hwnd, false, WindowStyle.TiledWindow);
|
||||
unsafe
|
||||
{
|
||||
BOOL value = false;
|
||||
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_WINDOW_CORNER_PREFERENCE, &value, (uint)sizeof(BOOL));
|
||||
}
|
||||
}
|
||||
|
||||
private HWND GetWindowHandle(Window window)
|
||||
{
|
||||
var hwnd = WindowNative.GetWindowHandle(window);
|
||||
return new HWND(hwnd);
|
||||
}
|
||||
|
||||
private void UpdateSettings()
|
||||
{
|
||||
this.viewModel.UpdateSettings(_settings);
|
||||
|
||||
SystemBackdrop = DockSettingsToViews.GetSystemBackdrop(_settings.Backdrop);
|
||||
|
||||
// If the backdrop is acrylic, things are more complicated
|
||||
if (_settings.Backdrop == DockBackdrop.Acrylic)
|
||||
{
|
||||
SetAcrylic();
|
||||
}
|
||||
|
||||
_dock.UpdateSettings(_settings);
|
||||
var side = DockSettingsToViews.GetAppBarEdge(_settings.Side);
|
||||
|
||||
if (_appBarData.hWnd != IntPtr.Zero)
|
||||
{
|
||||
var sameEdge = _appBarData.uEdge == side;
|
||||
var sameSize = _lastSize == _settings.DockSize;
|
||||
if (sameEdge && sameSize)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DestroyAppBar(_hwnd);
|
||||
}
|
||||
|
||||
CreateAppBar(_hwnd);
|
||||
}
|
||||
|
||||
// We want to use DesktopAcrylicKind.Thin and custom colors as this is the default material
|
||||
// other Shell surfaces are using, this cannot be set in XAML however.
|
||||
private void SetAcrylic()
|
||||
{
|
||||
if (DesktopAcrylicController.IsSupported())
|
||||
{
|
||||
// Hooking up the policy object.
|
||||
_configurationSource = new SystemBackdropConfiguration
|
||||
{
|
||||
// Initial configuration state.
|
||||
IsInputActive = true,
|
||||
};
|
||||
UpdateAcrylic();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAcrylic()
|
||||
{
|
||||
if (_acrylicController != null)
|
||||
{
|
||||
_acrylicController.RemoveAllSystemBackdropTargets();
|
||||
_acrylicController.Dispose();
|
||||
}
|
||||
|
||||
_acrylicController = GetAcrylicConfig(Content);
|
||||
|
||||
// Enable the system backdrop.
|
||||
// Note: Be sure to have "using WinRT;" to support the Window.As<...>() call.
|
||||
_acrylicController.AddSystemBackdropTarget(this.As<ICompositionSupportsSystemBackdrop>());
|
||||
_acrylicController.SetSystemBackdropConfiguration(_configurationSource);
|
||||
}
|
||||
|
||||
private void DisposeAcrylic()
|
||||
{
|
||||
if (_acrylicController is not null)
|
||||
{
|
||||
_acrylicController.Dispose();
|
||||
_acrylicController = null!;
|
||||
_configurationSource = null!;
|
||||
}
|
||||
}
|
||||
|
||||
private static DesktopAcrylicController GetAcrylicConfig(UIElement content)
|
||||
{
|
||||
var feContent = content as FrameworkElement;
|
||||
|
||||
return feContent?.ActualTheme == ElementTheme.Light
|
||||
? new DesktopAcrylicController()
|
||||
{
|
||||
Kind = DesktopAcrylicKind.Thin,
|
||||
TintColor = Color.FromArgb(255, 243, 243, 243),
|
||||
LuminosityOpacity = 0.90f,
|
||||
TintOpacity = 0.0f,
|
||||
FallbackColor = Color.FromArgb(255, 238, 238, 238),
|
||||
}
|
||||
: new DesktopAcrylicController()
|
||||
{
|
||||
Kind = DesktopAcrylicKind.Thin,
|
||||
TintColor = Color.FromArgb(255, 32, 32, 32),
|
||||
LuminosityOpacity = 0.96f,
|
||||
TintOpacity = 0.5f,
|
||||
FallbackColor = Color.FromArgb(255, 28, 28, 28),
|
||||
};
|
||||
}
|
||||
|
||||
private void CreateAppBar(HWND hwnd)
|
||||
{
|
||||
_appBarData = new APPBARDATA
|
||||
{
|
||||
cbSize = (uint)Marshal.SizeOf<APPBARDATA>(),
|
||||
hWnd = hwnd,
|
||||
uCallbackMessage = _callbackMessageId,
|
||||
};
|
||||
|
||||
// Register this window as an appbar
|
||||
PInvoke.SHAppBarMessage(PInvoke.ABM_NEW, ref _appBarData);
|
||||
|
||||
// Stash the last size we created the bar at, so we know when to hot-
|
||||
// reload it
|
||||
_lastSize = _settings.DockSize;
|
||||
|
||||
UpdateWindowPosition();
|
||||
}
|
||||
|
||||
private void DestroyAppBar(HWND hwnd)
|
||||
{
|
||||
PInvoke.SHAppBarMessage(PInvoke.ABM_REMOVE, ref _appBarData);
|
||||
_appBarData = default;
|
||||
}
|
||||
|
||||
private void UpdateWindowPosition()
|
||||
{
|
||||
Logger.LogDebug("UpdateWindowPosition");
|
||||
|
||||
var dpi = PInvoke.GetDpiForWindow(_hwnd);
|
||||
|
||||
var screenWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN);
|
||||
|
||||
// Get system border metrics
|
||||
var borderWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXBORDER);
|
||||
var edgeWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXEDGE);
|
||||
var frameWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXFRAME);
|
||||
|
||||
UpdateAppBarDataForEdge(_settings.Side, _settings.DockSize, dpi / 96.0);
|
||||
|
||||
// Query and set position
|
||||
PInvoke.SHAppBarMessage(PInvoke.ABM_QUERYPOS, ref _appBarData);
|
||||
PInvoke.SHAppBarMessage(PInvoke.ABM_SETPOS, ref _appBarData);
|
||||
|
||||
// TODO: investigate ABS_AUTOHIDE and autohide bars.
|
||||
// I think it's something like this, but I don't totally know
|
||||
// // _appBarData.lParam = ABS_ALWAYSONTOP;
|
||||
// _appBarData.lParam = (LPARAM)(int)PInvoke.ABS_AUTOHIDE;
|
||||
// PInvoke.SHAppBarMessage(ABM_SETSTATE, ref _appBarData);
|
||||
// PInvoke.SHAppBarMessage(PInvoke.ABM_SETAUTOHIDEBAR, ref _appBarData);
|
||||
|
||||
// Account for system borders when moving the window
|
||||
// Adjust position to account for window frame/border
|
||||
var adjustedLeft = _appBarData.rc.left - frameWidth;
|
||||
var adjustedTop = _appBarData.rc.top - frameWidth;
|
||||
var adjustedWidth = (_appBarData.rc.right - _appBarData.rc.left) + (2 * frameWidth);
|
||||
var adjustedHeight = (_appBarData.rc.bottom - _appBarData.rc.top) + (2 * frameWidth);
|
||||
|
||||
// Move the actual window
|
||||
PInvoke.MoveWindow(
|
||||
_hwnd,
|
||||
adjustedLeft,
|
||||
adjustedTop,
|
||||
adjustedWidth,
|
||||
adjustedHeight,
|
||||
true);
|
||||
}
|
||||
|
||||
private void UpdateAppBarDataForEdge(DockSide side, DockSize size, double scaleFactor)
|
||||
{
|
||||
Logger.LogDebug("UpdateAppBarDataForEdge");
|
||||
var horizontalHeightDips = DockSettingsToViews.HeightForSize(size);
|
||||
var verticalWidthDips = DockSettingsToViews.WidthForSize(size);
|
||||
var screenHeight = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYSCREEN);
|
||||
var screenWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN);
|
||||
|
||||
if (side == DockSide.Top)
|
||||
{
|
||||
_appBarData.uEdge = PInvoke.ABE_TOP;
|
||||
_appBarData.rc.left = 0;
|
||||
_appBarData.rc.top = 0;
|
||||
_appBarData.rc.right = screenWidth;
|
||||
_appBarData.rc.bottom = (int)(horizontalHeightDips * scaleFactor);
|
||||
}
|
||||
else if (side == DockSide.Bottom)
|
||||
{
|
||||
var heightPixels = (int)(horizontalHeightDips * scaleFactor);
|
||||
|
||||
_appBarData.uEdge = PInvoke.ABE_BOTTOM;
|
||||
_appBarData.rc.left = 0;
|
||||
_appBarData.rc.top = screenHeight - heightPixels;
|
||||
_appBarData.rc.right = screenWidth;
|
||||
_appBarData.rc.bottom = screenHeight;
|
||||
}
|
||||
else if (side == DockSide.Left)
|
||||
{
|
||||
var widthPixels = (int)(verticalWidthDips * scaleFactor);
|
||||
|
||||
_appBarData.uEdge = PInvoke.ABE_LEFT;
|
||||
_appBarData.rc.left = 0;
|
||||
_appBarData.rc.top = 0;
|
||||
_appBarData.rc.right = widthPixels;
|
||||
_appBarData.rc.bottom = screenHeight;
|
||||
}
|
||||
else if (side == DockSide.Right)
|
||||
{
|
||||
var widthPixels = (int)(verticalWidthDips * scaleFactor);
|
||||
|
||||
_appBarData.uEdge = PInvoke.ABE_RIGHT;
|
||||
_appBarData.rc.left = screenWidth - widthPixels;
|
||||
_appBarData.rc.top = 0;
|
||||
_appBarData.rc.right = screenWidth;
|
||||
_appBarData.rc.bottom = screenHeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private LRESULT CustomWndProc(HWND hwnd, uint msg, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
// check settings changed
|
||||
if (msg == PInvoke.WM_SETTINGCHANGE)
|
||||
{
|
||||
var isFullscreen = IsWindowFullscreen();
|
||||
|
||||
Logger.LogDebug($"WM_SETTINGCHANGE ({isFullscreen})");
|
||||
|
||||
if (isFullscreen)
|
||||
{
|
||||
this.Hide();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Show();
|
||||
}
|
||||
|
||||
if (wParam == (uint)SYSTEM_PARAMETERS_INFO_ACTION.SPI_SETWORKAREA)
|
||||
{
|
||||
Logger.LogDebug($"WM_SETTINGCHANGE(SPI_SETWORKAREA)");
|
||||
|
||||
// Use debounced call to throttle rapid successive calls
|
||||
DispatcherQueue.TryEnqueue(() => UpdateWindowPosition());
|
||||
}
|
||||
}
|
||||
else if (msg == PInvoke.WM_DISPLAYCHANGE)
|
||||
{
|
||||
Logger.LogDebug("WM_DISPLAYCHANGE");
|
||||
|
||||
// Use dispatcher to ensure we're on the UI thread
|
||||
DispatcherQueue.TryEnqueue(() => UpdateWindowPosition());
|
||||
}
|
||||
|
||||
// Intercept WM_SYSCOMMAND to prevent minimize and maximize
|
||||
else if (msg == PInvoke.WM_SYSCOMMAND)
|
||||
{
|
||||
var command = (int)(wParam.Value & 0xFFF0);
|
||||
if (command == PInvoke.SC_MINIMIZE || command == PInvoke.SC_MAXIMIZE)
|
||||
{
|
||||
// Block minimize and maximize commands
|
||||
return new LRESULT(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Stop min/max on WM_WINDOWPOSCHANGING too
|
||||
else if (msg == PInvoke.WM_WINDOWPOSCHANGING)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var pWindowPos = (WINDOWPOS*)lParam.Value;
|
||||
|
||||
// Check if the window is being hidden (minimized) or if flags suggest minimize/maximize
|
||||
if ((pWindowPos->flags & SET_WINDOW_POS_FLAGS.SWP_HIDEWINDOW) != 0)
|
||||
{
|
||||
// Prevent hiding the window (minimize)
|
||||
pWindowPos->flags &= ~SET_WINDOW_POS_FLAGS.SWP_HIDEWINDOW;
|
||||
pWindowPos->flags |= SET_WINDOW_POS_FLAGS.SWP_SHOWWINDOW;
|
||||
}
|
||||
|
||||
// Additional check: if the window position suggests it's being minimized or maximized
|
||||
// by checking for dramatic size changes
|
||||
if (pWindowPos->cx <= 0 || pWindowPos->cy <= 0)
|
||||
{
|
||||
// Prevent zero or negative size changes (minimize)
|
||||
pWindowPos->flags |= SET_WINDOW_POS_FLAGS.SWP_NOSIZE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle WM_SIZE to prevent minimize/maximize state changes
|
||||
else if (msg == PInvoke.WM_SIZE)
|
||||
{
|
||||
var sizeType = (int)wParam.Value;
|
||||
if (sizeType == PInvoke.SIZE_MINIMIZED || sizeType == PInvoke.SIZE_MAXIMIZED)
|
||||
{
|
||||
// Block the size change by not calling the original window procedure
|
||||
return new LRESULT(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle WM_SHOWWINDOW to prevent hiding (minimize)
|
||||
else if (msg == PInvoke.WM_SHOWWINDOW)
|
||||
{
|
||||
var isBeingShown = wParam.Value != 0;
|
||||
if (!isBeingShown)
|
||||
{
|
||||
// Prevent hiding the window
|
||||
return new LRESULT(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle double-click on title bar (non-client area)
|
||||
else if (msg == PInvoke.WM_NCLBUTTONDBLCLK)
|
||||
{
|
||||
var hitTest = (int)wParam.Value;
|
||||
if (hitTest == PInvoke.HTCAPTION)
|
||||
{
|
||||
// Block double-click on title bar to prevent maximize
|
||||
return new LRESULT(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle WM_GETMINMAXINFO to control window size limits
|
||||
else if (msg == PInvoke.WM_GETMINMAXINFO)
|
||||
{
|
||||
// We can modify the min/max tracking info here if needed
|
||||
// For now, let it pass through but we could restrict max size
|
||||
}
|
||||
|
||||
// Handle the AppBarMessage message
|
||||
// This is needed to update the position when the work area changes.
|
||||
// (notably, when the user toggles auto-hide taskbars)
|
||||
else if (msg == _callbackMessageId)
|
||||
{
|
||||
if (wParam.Value == PInvoke.ABN_POSCHANGED)
|
||||
{
|
||||
UpdateWindowPosition();
|
||||
}
|
||||
}
|
||||
else if (msg == WM_TASKBAR_RESTART)
|
||||
{
|
||||
Logger.LogDebug("WM_TASKBAR_RESTART");
|
||||
|
||||
DispatcherQueue.TryEnqueue(() => CreateAppBar(_hwnd));
|
||||
|
||||
WeakReferenceMessenger.Default.Send<BringToTopMessage>(new(false));
|
||||
}
|
||||
|
||||
// Call the original window procedure for all other messages
|
||||
return PInvoke.CallWindowProc(_originalWndProc, hwnd, msg, wParam, lParam);
|
||||
}
|
||||
|
||||
void IRecipient<BringToTopMessage>.Receive(BringToTopMessage message)
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
var onTop = message.OnTop ? HWND.HWND_TOPMOST : HWND.HWND_NOTOPMOST;
|
||||
PInvoke.SetWindowPos(_hwnd, onTop, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
|
||||
PInvoke.SetWindowPos(_hwnd, HWND.HWND_NOTOPMOST, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
|
||||
});
|
||||
}
|
||||
|
||||
public static bool IsWindowFullscreen()
|
||||
{
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ne-shellapi-query_user_notification_state
|
||||
if (Marshal.GetExceptionForHR(PInvoke.SHQueryUserNotificationState(out var state)) is null)
|
||||
{
|
||||
if (state == QUERY_USER_NOTIFICATION_STATE.QUNS_RUNNING_D3D_FULL_SCREEN ||
|
||||
state == QUERY_USER_NOTIFICATION_STATE.QUNS_BUSY ||
|
||||
state == QUERY_USER_NOTIFICATION_STATE.QUNS_PRESENTATION_MODE)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Receive(QuitMessage message)
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
DestroyAppBar(_hwnd);
|
||||
|
||||
this.Close();
|
||||
});
|
||||
}
|
||||
|
||||
void IRecipient<RequestShowPaletteAtMessage>.Receive(RequestShowPaletteAtMessage message)
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(() => RequestShowPaletteOnUiThread(message.PosDips));
|
||||
}
|
||||
|
||||
private void RequestShowPaletteOnUiThread(Point posDips)
|
||||
{
|
||||
// pos is relative to our root. We need to convert to screen coords.
|
||||
var rootPosDips = Root.TransformToVisual(null).TransformPoint(new Point(0, 0));
|
||||
var screenPosDips = new Point(rootPosDips.X + posDips.X, rootPosDips.Y + posDips.Y);
|
||||
|
||||
var dpi = PInvoke.GetDpiForWindow(_hwnd);
|
||||
var scaleFactor = dpi / 96.0;
|
||||
var screenPosPixels = new Point(screenPosDips.X * scaleFactor, screenPosDips.Y * scaleFactor);
|
||||
|
||||
var screenWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN);
|
||||
var screenHeight = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYSCREEN);
|
||||
|
||||
// Now we're going to find the best position for the palette.
|
||||
|
||||
// We want to anchor the palette on the dock side.
|
||||
// on the top:
|
||||
// - anchor to the top, left if we're on the left half of the screen
|
||||
// - anchor to the top, right if we're on the right half of the screen
|
||||
// On the left:
|
||||
// - anchor to the top, left if we're on the top half of the screen
|
||||
// - anchor to the bottom, left if we're on the bottom half of the screen
|
||||
// On the right:
|
||||
// - anchor to the top, right if we're on the top half of the screen
|
||||
// - anchor to the bottom, right if we're on the bottom half of the screen
|
||||
// On the bottom:
|
||||
// - anchor to the bottom, left if we're on the left half of the screen
|
||||
// - anchor to the bottom, right if we're on the right half of the screen
|
||||
var onTopHalf = screenPosPixels.Y < screenHeight / 2;
|
||||
var onLeftHalf = screenPosPixels.X < screenWidth / 2;
|
||||
var onRightHalf = !onLeftHalf;
|
||||
var onBottomHalf = !onTopHalf;
|
||||
|
||||
var anchorPoint = _settings.Side switch
|
||||
{
|
||||
DockSide.Top => onLeftHalf ? AnchorPoint.TopLeft : AnchorPoint.TopRight,
|
||||
DockSide.Bottom => onLeftHalf ? AnchorPoint.BottomLeft : AnchorPoint.BottomRight,
|
||||
DockSide.Left => onTopHalf ? AnchorPoint.TopLeft : AnchorPoint.BottomLeft,
|
||||
DockSide.Right => onTopHalf ? AnchorPoint.TopRight : AnchorPoint.BottomRight,
|
||||
_ => AnchorPoint.TopLeft,
|
||||
};
|
||||
|
||||
// we also need to slide the anchor point a bit away from the dock
|
||||
var paddingDips = 8;
|
||||
var paddingPixels = paddingDips * scaleFactor;
|
||||
PInvoke.GetWindowRect(_hwnd, out var ourRect);
|
||||
|
||||
// Depending on the side we're on, we need to offset differently
|
||||
switch (_settings.Side)
|
||||
{
|
||||
case DockSide.Top:
|
||||
screenPosPixels.Y = ourRect.bottom + paddingPixels;
|
||||
break;
|
||||
case DockSide.Bottom:
|
||||
screenPosPixels.Y = ourRect.top - paddingPixels;
|
||||
break;
|
||||
case DockSide.Left:
|
||||
screenPosPixels.X = ourRect.right + paddingPixels;
|
||||
break;
|
||||
case DockSide.Right:
|
||||
screenPosPixels.X = ourRect.left - paddingPixels;
|
||||
break;
|
||||
}
|
||||
|
||||
// Now that we know the anchor corner, and where to attempt to place it, we can
|
||||
// ask the palette to show itself there.
|
||||
WeakReferenceMessenger.Default.Send<ShowPaletteAtMessage>(new(screenPosPixels, anchorPoint));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisposeAcrylic();
|
||||
viewModel.Dispose();
|
||||
}
|
||||
|
||||
private void DockWindow_Closed(object sender, WindowEventArgs args)
|
||||
{
|
||||
var serviceProvider = App.Current.Services;
|
||||
var settings = serviceProvider.GetService<SettingsModel>();
|
||||
settings?.SettingsChanged -= SettingsChangedHandler;
|
||||
DisposeAcrylic();
|
||||
|
||||
// Remove our appbar registration
|
||||
DestroyAppBar(_hwnd);
|
||||
|
||||
// Unhook the window procedure
|
||||
ShowDesktop.RemoveHook();
|
||||
}
|
||||
}
|
||||
|
||||
// Thank you to https://stackoverflow.com/a/35422795/1481137
|
||||
internal static class ShowDesktop
|
||||
{
|
||||
private const string WORKERW = "WorkerW";
|
||||
private const string PROGMAN = "Progman";
|
||||
|
||||
private static WINEVENTPROC? _hookProc;
|
||||
private static IntPtr _hookHandle = IntPtr.Zero;
|
||||
|
||||
public static void AddHook(Window window)
|
||||
{
|
||||
if (IsHooked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsHooked = true;
|
||||
|
||||
_hookProc = (WINEVENTPROC)WinEventCallback;
|
||||
_hookHandle = PInvoke.SetWinEventHook(PInvoke.EVENT_SYSTEM_FOREGROUND, PInvoke.EVENT_SYSTEM_FOREGROUND, HMODULE.Null, _hookProc, 0, 0, PInvoke.WINEVENT_OUTOFCONTEXT);
|
||||
}
|
||||
|
||||
public static void RemoveHook()
|
||||
{
|
||||
if (!IsHooked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsHooked = false;
|
||||
|
||||
PInvoke.UnhookWinEvent((HWINEVENTHOOK)_hookHandle);
|
||||
_hookProc = null;
|
||||
_hookHandle = IntPtr.Zero;
|
||||
}
|
||||
|
||||
private static string GetWindowClass(HWND hwnd)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
fixed (char* c = new char[32])
|
||||
{
|
||||
_ = PInvoke.GetClassName(hwnd, (PWSTR)c, 32);
|
||||
return new string(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
|
||||
|
||||
private static void WinEventCallback(
|
||||
HWINEVENTHOOK hWinEventHook,
|
||||
uint eventType,
|
||||
HWND hwnd,
|
||||
int idObject,
|
||||
int idChild,
|
||||
uint dwEventThread,
|
||||
uint dwmsEventTime)
|
||||
{
|
||||
if (eventType == PInvoke.EVENT_SYSTEM_FOREGROUND)
|
||||
{
|
||||
var @class = GetWindowClass(hwnd);
|
||||
if (string.Equals(@class, WORKERW, StringComparison.Ordinal) || string.Equals(@class, PROGMAN, StringComparison.Ordinal))
|
||||
{
|
||||
Logger.LogDebug("ShowDesktop invoked. Bring us back");
|
||||
WeakReferenceMessenger.Default.Send<BringToTopMessage>(new(true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsHooked { get; private set; }
|
||||
}
|
||||
|
||||
internal sealed record BringToTopMessage(bool OnTop);
|
||||
|
||||
internal sealed record RequestShowPaletteAtMessage(Point PosDips);
|
||||
|
||||
internal sealed record ShowPaletteAtMessage(Point PosPixels, AnchorPoint Anchor);
|
||||
|
||||
internal enum AnchorPoint
|
||||
{
|
||||
TopLeft,
|
||||
TopRight,
|
||||
BottomLeft,
|
||||
BottomRight,
|
||||
}
|
||||
|
||||
#pragma warning restore SA1402 // File may only contain a single type
|
||||
@@ -1,34 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Dock;
|
||||
|
||||
public sealed partial class IconInfoVisibilityConverter : IValueConverter
|
||||
{
|
||||
private static bool IsVisible(IconInfoViewModel iconInfoViewModel, ElementTheme requestedTheme) =>
|
||||
iconInfoViewModel?.HasIcon(requestedTheme == Microsoft.UI.Xaml.ElementTheme.Light) ?? false;
|
||||
|
||||
private static bool IsVisible(IconInfoViewModel iconInfoViewModel, ApplicationTheme requestedTheme) =>
|
||||
iconInfoViewModel?.HasIcon(requestedTheme == ApplicationTheme.Light) ?? false;
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is IconInfoViewModel iconInfoVM)
|
||||
{
|
||||
return IsVisible(iconInfoVM, Application.Current.RequestedTheme) ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
return Visibility.Visible;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException();
|
||||
|
||||
public IconInfoVisibilityConverter()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -587,7 +587,7 @@ public sealed partial class ListPage : Page,
|
||||
var shouldUpdateSelection = false;
|
||||
|
||||
// If it's a top level list update we force the reset to the top useful item
|
||||
if (sender.IsRootPage)
|
||||
if (!sender.IsNested)
|
||||
{
|
||||
shouldUpdateSelection = true;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -152,7 +152,7 @@ internal sealed partial class TrayIconService
|
||||
{
|
||||
if (wParam == PInvoke.WM_USER + 1)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>(new());
|
||||
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>();
|
||||
}
|
||||
else if (wParam == PInvoke.WM_USER + 2)
|
||||
{
|
||||
|
||||
@@ -12,7 +12,6 @@ using Microsoft.CmdPal.Core.Common.Services;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Messages;
|
||||
using Microsoft.CmdPal.UI.Controls;
|
||||
using Microsoft.CmdPal.UI.Dock;
|
||||
using Microsoft.CmdPal.UI.Events;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
@@ -51,7 +50,6 @@ namespace Microsoft.CmdPal.UI;
|
||||
public sealed partial class MainWindow : WindowEx,
|
||||
IRecipient<DismissMessage>,
|
||||
IRecipient<ShowWindowMessage>,
|
||||
IRecipient<ShowPaletteAtMessage>,
|
||||
IRecipient<HideWindowMessage>,
|
||||
IRecipient<QuitMessage>,
|
||||
IRecipient<NavigateToPageMessage>,
|
||||
@@ -136,7 +134,6 @@ public sealed partial class MainWindow : WindowEx,
|
||||
WeakReferenceMessenger.Default.Register<DismissMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<QuitMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<ShowWindowMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<ShowPaletteAtMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<HideWindowMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigateToPageMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigationDepthMessage>(this);
|
||||
@@ -335,77 +332,6 @@ public sealed partial class MainWindow : WindowEx,
|
||||
}
|
||||
|
||||
private void ShowHwnd(IntPtr hwndValue, MonitorBehavior target)
|
||||
{
|
||||
var positionWindowForTargetMonitor = (HWND hwnd) =>
|
||||
{
|
||||
if (target == MonitorBehavior.ToLast)
|
||||
{
|
||||
var newRect = EnsureWindowIsVisible(_currentWindowPosition.ToPhysicalWindowRectangle(), new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight), _currentWindowPosition.Dpi);
|
||||
AppWindow.MoveAndResize(newRect);
|
||||
}
|
||||
else
|
||||
{
|
||||
var display = GetScreen(hwnd, target);
|
||||
PositionCentered(display);
|
||||
}
|
||||
};
|
||||
ShowHwnd(hwndValue, positionWindowForTargetMonitor);
|
||||
}
|
||||
|
||||
private void ShowHwnd(IntPtr hwndValue, Point anchorInPixels, AnchorPoint anchorCorner)
|
||||
{
|
||||
var positionWindowForAnchor = (HWND hwnd) =>
|
||||
{
|
||||
PInvoke.GetWindowRect(hwnd, out var bounds);
|
||||
var swpFlags = SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE | SET_WINDOW_POS_FLAGS.SWP_NOZORDER;
|
||||
switch (anchorCorner)
|
||||
{
|
||||
case AnchorPoint.TopLeft:
|
||||
PInvoke.SetWindowPos(
|
||||
hwnd,
|
||||
HWND.HWND_TOP,
|
||||
(int)anchorInPixels.X,
|
||||
(int)anchorInPixels.Y,
|
||||
0,
|
||||
0,
|
||||
swpFlags);
|
||||
break;
|
||||
case AnchorPoint.TopRight:
|
||||
PInvoke.SetWindowPos(
|
||||
hwnd,
|
||||
HWND.HWND_TOP,
|
||||
(int)(anchorInPixels.X - bounds.Width),
|
||||
(int)anchorInPixels.Y,
|
||||
0,
|
||||
0,
|
||||
swpFlags);
|
||||
break;
|
||||
case AnchorPoint.BottomLeft:
|
||||
PInvoke.SetWindowPos(
|
||||
hwnd,
|
||||
HWND.HWND_TOP,
|
||||
(int)anchorInPixels.X,
|
||||
(int)(anchorInPixels.Y - bounds.Height),
|
||||
0,
|
||||
0,
|
||||
swpFlags);
|
||||
break;
|
||||
case AnchorPoint.BottomRight:
|
||||
PInvoke.SetWindowPos(
|
||||
hwnd,
|
||||
HWND.HWND_TOP,
|
||||
(int)(anchorInPixels.X - bounds.Width),
|
||||
(int)(anchorInPixels.Y - bounds.Height),
|
||||
0,
|
||||
0,
|
||||
swpFlags);
|
||||
break;
|
||||
}
|
||||
};
|
||||
ShowHwnd(hwndValue, positionWindowForAnchor);
|
||||
}
|
||||
|
||||
private void ShowHwnd(IntPtr hwndValue, Action<HWND>? positionWindow)
|
||||
{
|
||||
StopAutoGoHome();
|
||||
|
||||
@@ -424,9 +350,15 @@ public sealed partial class MainWindow : WindowEx,
|
||||
PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_RESTORE);
|
||||
}
|
||||
|
||||
if (positionWindow is not null)
|
||||
if (target == MonitorBehavior.ToLast)
|
||||
{
|
||||
positionWindow(hwnd);
|
||||
var newRect = EnsureWindowIsVisible(_currentWindowPosition.ToPhysicalWindowRectangle(), new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight), _currentWindowPosition.Dpi);
|
||||
AppWindow.MoveAndResize(newRect);
|
||||
}
|
||||
else
|
||||
{
|
||||
var display = GetScreen(hwnd, target);
|
||||
PositionCentered(display);
|
||||
}
|
||||
|
||||
// Check if the debugger is attached. If it is, we don't want to apply the tool window style,
|
||||
@@ -616,11 +548,6 @@ public sealed partial class MainWindow : WindowEx,
|
||||
ShowHwnd(message.Hwnd, settings.SummonOn);
|
||||
}
|
||||
|
||||
internal void Receive(ShowPaletteAtMessage message)
|
||||
{
|
||||
ShowHwnd(HWND.Null, message.PosPixels, message.Anchor);
|
||||
}
|
||||
|
||||
public void Receive(HideWindowMessage message)
|
||||
{
|
||||
// This might come in off the UI thread. Make sure to hop back.
|
||||
@@ -731,8 +658,6 @@ public sealed partial class MainWindow : WindowEx,
|
||||
// Sure, it's not ideal, but at least it's not visible.
|
||||
}
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new WindowHiddenMessage());
|
||||
|
||||
// Start auto-go-home timer
|
||||
RestartAutoGoHome();
|
||||
}
|
||||
@@ -1145,7 +1070,6 @@ public sealed partial class MainWindow : WindowEx,
|
||||
// but that's the price to pay for having the HWND not light-dismiss while we're debugging.
|
||||
Cloak();
|
||||
this.Hide();
|
||||
WeakReferenceMessenger.Default.Send(new WindowHiddenMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -1196,8 +1120,6 @@ public sealed partial class MainWindow : WindowEx,
|
||||
DisposeAcrylic();
|
||||
}
|
||||
|
||||
void IRecipient<ShowPaletteAtMessage>.Receive(ShowPaletteAtMessage message) => Receive(message);
|
||||
|
||||
public void Receive(DragStartedMessage message)
|
||||
{
|
||||
_preventHideWhenDeactivated = true;
|
||||
|
||||
@@ -19,9 +19,6 @@
|
||||
|
||||
<Version>$(CmdPalVersion)</Version>
|
||||
|
||||
<!-- For MVVM Toolkit Partial Properties/AOT support -->
|
||||
<LangVersion>preview</LangVersion>
|
||||
|
||||
<!-- OutputPath is set in CmdPal.Branding.props -->
|
||||
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
@@ -30,10 +27,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- For debugging purposes, uncomment this block to enable AOT builds -->
|
||||
<PropertyGroup>
|
||||
<!--<PropertyGroup>
|
||||
<EnableCmdPalAOT>true</EnableCmdPalAOT>
|
||||
<GeneratePackageLocally>true</GeneratePackageLocally>
|
||||
</PropertyGroup>
|
||||
</PropertyGroup>-->
|
||||
|
||||
<PropertyGroup Condition="'$(EnableCmdPalAOT)' == 'true'">
|
||||
<SelfContained>true</SelfContained>
|
||||
@@ -77,7 +74,6 @@
|
||||
<None Remove="Controls\FallbackRankerDialog.xaml" />
|
||||
<None Remove="Controls\KeyVisual\KeyCharPresenter.xaml" />
|
||||
<None Remove="Controls\ScreenPreview.xaml" />
|
||||
<None Remove="Controls\ScrollContainer.xaml" />
|
||||
<None Remove="Controls\SearchBar.xaml" />
|
||||
<None Remove="IsEnabledTextBlock.xaml" />
|
||||
<None Remove="ListDetailPage.xaml" />
|
||||
@@ -88,7 +84,6 @@
|
||||
<None Remove="SettingsWindow.xaml" />
|
||||
<None Remove="Settings\AppearancePage.xaml" />
|
||||
<None Remove="ShellPage.xaml" />
|
||||
<None Remove="Styles\Button.xaml" />
|
||||
<None Remove="Styles\Colors.xaml" />
|
||||
<None Remove="Styles\Settings.xaml" />
|
||||
<None Remove="Styles\TextBox.xaml" />
|
||||
@@ -145,7 +140,6 @@
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Apps\Microsoft.CmdPal.Ext.Apps.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Bookmark\Microsoft.CmdPal.Ext.Bookmarks.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Calc\Microsoft.CmdPal.Ext.Calc.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.PerformanceMonitor\Microsoft.CmdPal.Ext.PerformanceMonitor.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Registry\Microsoft.CmdPal.Ext.Registry.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Shell\Microsoft.CmdPal.Ext.Shell.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.TimeDate\Microsoft.CmdPal.Ext.TimeDate.csproj" />
|
||||
@@ -220,18 +214,6 @@
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="Controls\ScrollContainer.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="Styles\Button.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="Controls\CommandPalettePreview.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
|
||||
@@ -59,52 +59,11 @@ GetModuleHandle
|
||||
GetWindowLong
|
||||
SetWindowLong
|
||||
WINDOW_EX_STYLE
|
||||
|
||||
CreateWindowEx
|
||||
WNDCLASSEXW
|
||||
RegisterClassEx
|
||||
GetStockObject
|
||||
GetModuleHandle
|
||||
|
||||
MoveWindow
|
||||
GetSystemMetrics
|
||||
SHAppBarMessage
|
||||
ABM_NEW
|
||||
ABM_QUERYPOS
|
||||
ABM_SETPOS
|
||||
ABM_REMOVE
|
||||
ABM_SETAUTOHIDEBAR
|
||||
ABS_AUTOHIDE
|
||||
ABN_POSCHANGED
|
||||
APPBARDATA
|
||||
ABE_TOP
|
||||
ABE_BOTTOM
|
||||
ABE_LEFT
|
||||
ABE_RIGHT
|
||||
SYSTEM_METRICS_INDEX
|
||||
GetDpiForWindow
|
||||
SHQueryUserNotificationState
|
||||
SYSTEM_PARAMETERS_INFO_ACTION
|
||||
WINDOWPOS
|
||||
WM_DISPLAYCHANGE
|
||||
WM_SYSCOMMAND
|
||||
WM_SETTINGCHANGE
|
||||
WM_WINDOWPOSCHANGING
|
||||
WM_SHOWWINDOW
|
||||
WM_SIZE
|
||||
WM_GETMINMAXINFO
|
||||
SetWinEventHook
|
||||
WINDOW_STYLE
|
||||
SC_MINIMIZE
|
||||
SC_MAXIMIZE
|
||||
SET_WINDOW_POS_FLAGS
|
||||
SIZE_MAXIMIZED
|
||||
SIZE_MINIMIZED
|
||||
HWND_NOTOPMOST
|
||||
HWND_TOP
|
||||
HTCAPTION
|
||||
GetClassName
|
||||
EVENT_SYSTEM_FOREGROUND
|
||||
WINEVENT_OUTOFCONTEXT
|
||||
GetWindowThreadProcessId
|
||||
AttachThreadInput
|
||||
AttachThreadInput
|
||||
@@ -200,19 +200,14 @@
|
||||
|
||||
<!-- Back button -->
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<!--
|
||||
This border is to hold a bit of padding we need when
|
||||
the back button is hidden
|
||||
-->
|
||||
<Border Margin="20,0,0,0" Visibility="{x:Bind ViewModel.CurrentPage.HasBackButton, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}" />
|
||||
<Image
|
||||
Width="20"
|
||||
Margin="0,0,6,0"
|
||||
Margin="20,0,6,0"
|
||||
HorizontalAlignment="Center"
|
||||
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Source="ms-appx:///Assets/icon.svg"
|
||||
Visibility="{x:Bind ViewModel.CurrentPage.IsRootPage, Mode=OneWay}">
|
||||
Visibility="{x:Bind ViewModel.CurrentPage.IsNested, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||
<animations:Implicit.ShowAnimations>
|
||||
<animations:OpacityAnimation
|
||||
EasingMode="EaseIn"
|
||||
@@ -255,7 +250,7 @@
|
||||
FontSize=14}"
|
||||
FontSize="16"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Visibility="{x:Bind ViewModel.CurrentPage.HasBackButton, Mode=OneWay}">
|
||||
Visibility="{x:Bind ViewModel.CurrentPage.IsNested, Mode=OneWay}">
|
||||
<animations:Implicit.ShowAnimations>
|
||||
<animations:OpacityAnimation
|
||||
EasingMode="EaseIn"
|
||||
@@ -302,7 +297,7 @@
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
SourceKey="{x:Bind ViewModel.CurrentPage.Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}"
|
||||
Visibility="{x:Bind ViewModel.CurrentPage.IsRootPage, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||
Visibility="{x:Bind ViewModel.CurrentPage.IsNested, Mode=OneWay}">
|
||||
<animations:Implicit.ShowAnimations>
|
||||
<animations:OpacityAnimation
|
||||
From="0"
|
||||
|
||||
@@ -10,13 +10,11 @@ using CommunityToolkit.WinUI;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.Dock;
|
||||
using Microsoft.CmdPal.UI.Events;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
using Microsoft.CmdPal.UI.Settings;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
@@ -27,7 +25,6 @@ using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media.Animation;
|
||||
using Windows.UI.Core;
|
||||
using WinUIEx;
|
||||
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
|
||||
using VirtualKey = Windows.System.VirtualKey;
|
||||
|
||||
@@ -50,7 +47,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
IRecipient<ShowConfirmationMessage>,
|
||||
IRecipient<ShowToastMessage>,
|
||||
IRecipient<NavigateToPageMessage>,
|
||||
IRecipient<ShowHideDockMessage>,
|
||||
INotifyPropertyChanged,
|
||||
IDisposable
|
||||
{
|
||||
@@ -68,7 +64,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
private readonly CompositeFormat _pageNavigatedAnnouncement;
|
||||
|
||||
private SettingsWindow? _settingsWindow;
|
||||
private DockWindow? _dockWindow;
|
||||
|
||||
private CancellationTokenSource? _focusAfterLoadedCts;
|
||||
private WeakReference<Page>? _lastNavigatedPageRef;
|
||||
@@ -99,8 +94,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
WeakReferenceMessenger.Default.Register<ShowToastMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigateToPageMessage>(this);
|
||||
|
||||
WeakReferenceMessenger.Default.Register<ShowHideDockMessage>(this);
|
||||
|
||||
AddHandler(PreviewKeyDownEvent, new KeyEventHandler(ShellPage_OnPreviewKeyDown), true);
|
||||
AddHandler(KeyDownEvent, new KeyEventHandler(ShellPage_OnKeyDown), false);
|
||||
AddHandler(PointerPressedEvent, new PointerEventHandler(ShellPage_OnPointerPressed), true);
|
||||
@@ -109,12 +102,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
var pageAnnouncementFormat = ResourceLoaderInstance.GetString("ScreenReader_Announcement_NavigatedToPage0");
|
||||
_pageNavigatedAnnouncement = CompositeFormat.Parse(pageAnnouncementFormat);
|
||||
|
||||
if (App.Current.Services.GetService<SettingsModel>()!.EnableDock)
|
||||
{
|
||||
_dockWindow = new DockWindow();
|
||||
_dockWindow.Show();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -261,29 +248,26 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeConfirmationDialog(ConfirmResultViewModel vm) => vm.SafeInitializePropertiesSynchronous();
|
||||
private void InitializeConfirmationDialog(ConfirmResultViewModel vm)
|
||||
{
|
||||
vm.SafeInitializePropertiesSynchronous();
|
||||
}
|
||||
|
||||
public void Receive(OpenSettingsMessage message)
|
||||
{
|
||||
_ = DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
OpenSettings(message.Page);
|
||||
OpenSettings();
|
||||
});
|
||||
}
|
||||
|
||||
public void OpenSettings(string? page = null)
|
||||
public void OpenSettings()
|
||||
{
|
||||
if (_settingsWindow is null)
|
||||
{
|
||||
_settingsWindow = new SettingsWindow();
|
||||
}
|
||||
|
||||
if (page is not null)
|
||||
{
|
||||
_settingsWindow.OpenToPage = page;
|
||||
_settingsWindow.Navigate(page);
|
||||
}
|
||||
|
||||
_settingsWindow.Activate();
|
||||
_settingsWindow.BringToFront();
|
||||
}
|
||||
@@ -344,7 +328,10 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
public void Receive(ClearSearchMessage message) => SearchBox.ClearSearch();
|
||||
|
||||
public void Receive(HotkeySummonMessage message) => _ = DispatcherQueue.TryEnqueue(() => SummonOnUiThread(message));
|
||||
public void Receive(HotkeySummonMessage message)
|
||||
{
|
||||
_ = DispatcherQueue.TryEnqueue(() => SummonOnUiThread(message));
|
||||
}
|
||||
|
||||
public void Receive(SettingsWindowClosedMessage message) => _settingsWindow = null;
|
||||
|
||||
@@ -413,7 +400,10 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
|
||||
}
|
||||
|
||||
public void Receive(GoBackMessage message) => _ = DispatcherQueue.TryEnqueue(() => GoBack(message.WithAnimation, message.FocusSearch));
|
||||
public void Receive(GoBackMessage message)
|
||||
{
|
||||
_ = DispatcherQueue.TryEnqueue(() => GoBack(message.WithAnimation, message.FocusSearch));
|
||||
}
|
||||
|
||||
private void GoBack(bool withAnimation = true, bool focusSearch = true)
|
||||
{
|
||||
@@ -454,7 +444,10 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(GoHomeMessage message) => _ = DispatcherQueue.TryEnqueue(() => GoHome(withAnimation: message.WithAnimation, focusSearch: message.FocusSearch));
|
||||
public void Receive(GoHomeMessage message)
|
||||
{
|
||||
_ = DispatcherQueue.TryEnqueue(() => GoHome(withAnimation: message.WithAnimation, focusSearch: message.FocusSearch));
|
||||
}
|
||||
|
||||
private void GoHome(bool withAnimation = true, bool focusSearch = true)
|
||||
{
|
||||
@@ -472,27 +465,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(ShowHideDockMessage message)
|
||||
{
|
||||
_ = DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
if (message.ShowDock)
|
||||
{
|
||||
if (_dockWindow is null)
|
||||
{
|
||||
_dockWindow = new DockWindow();
|
||||
}
|
||||
|
||||
_dockWindow.Show();
|
||||
}
|
||||
else if (_dockWindow is not null)
|
||||
{
|
||||
_dockWindow.Close();
|
||||
_dockWindow = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void BackButton_Clicked(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) => WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
|
||||
|
||||
private void RootFrame_Navigated(object sender, Microsoft.UI.Xaml.Navigation.NavigationEventArgs e)
|
||||
@@ -749,7 +721,5 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
_focusAfterLoadedCts?.Cancel();
|
||||
_focusAfterLoadedCts?.Dispose();
|
||||
_focusAfterLoadedCts = null;
|
||||
|
||||
_dockWindow?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
x:Class="Microsoft.CmdPal.UI.Settings.DockSettingsPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:cpControls="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:dockVm="using:Microsoft.CmdPal.UI.ViewModels.Dock"
|
||||
xmlns:helpers="using:Microsoft.CmdPal.UI.Helpers"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Settings"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
|
||||
mc:Ignorable="d">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<ScrollViewer Grid.Row="1">
|
||||
<Grid Padding="16">
|
||||
<StackPanel
|
||||
MaxWidth="1000"
|
||||
HorizontalAlignment="Stretch"
|
||||
Spacing="{StaticResource SettingsCardSpacing}">
|
||||
|
||||
<!--
|
||||
I got these from the samples, but they break XAML hot-reloading,
|
||||
so I commented them out.
|
||||
-->
|
||||
|
||||
<!--<StackPanel.ChildrenTransitions>
|
||||
<EntranceThemeTransition FromVerticalOffset="50" />
|
||||
<RepositionThemeTransition IsStaggeringEnabled="False" />
|
||||
</StackPanel.ChildrenTransitions>-->
|
||||
|
||||
<!-- Appearance Section -->
|
||||
<TextBlock x:Uid="DockAppearanceSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<!-- Dock Size -->
|
||||
<controls:SettingsCard Header="Dock Size">
|
||||
<controls:SettingsCard.Description>
|
||||
Choose the size of your dock
|
||||
</controls:SettingsCard.Description>
|
||||
<ComboBox
|
||||
x:Name="DockSizeComboBox"
|
||||
MinWidth="120"
|
||||
SelectedIndex="{x:Bind SelectedDockSizeIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem Content="Small" />
|
||||
<ComboBoxItem Content="Medium" />
|
||||
<ComboBoxItem Content="Large" />
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Dock Position -->
|
||||
<controls:SettingsCard Header="Dock Position">
|
||||
<controls:SettingsCard.HeaderIcon>
|
||||
<SymbolIcon Symbol="MoveToFolder" />
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
<controls:SettingsCard.Description>
|
||||
Choose where the dock appears on your screen
|
||||
</controls:SettingsCard.Description>
|
||||
<ComboBox
|
||||
x:Name="DockPositionComboBox"
|
||||
MinWidth="120"
|
||||
SelectedIndex="{x:Bind SelectedSideIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem Content="Left" />
|
||||
<ComboBoxItem Content="Top" />
|
||||
<ComboBoxItem Content="Right" />
|
||||
<ComboBoxItem Content="Bottom" />
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Backdrop Style -->
|
||||
<controls:SettingsCard Header="Background Style">
|
||||
<controls:SettingsCard.Description>
|
||||
Choose the background effect for your dock
|
||||
</controls:SettingsCard.Description>
|
||||
<ComboBox
|
||||
x:Name="BackdropComboBox"
|
||||
MinWidth="120"
|
||||
SelectedIndex="{x:Bind SelectedBackdropIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem Content="Mica" />
|
||||
<ComboBoxItem Content="Transparent" />
|
||||
<ComboBoxItem Content="Acrylic" />
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Show Labels -->
|
||||
<controls:SettingsCard Header="Show Labels">
|
||||
<controls:SettingsCard.Description>
|
||||
Choose whether to show labels for dock items by default.
|
||||
</controls:SettingsCard.Description>
|
||||
|
||||
<ToggleSwitch
|
||||
IsOn="{x:Bind ShowLabels, Mode=TwoWay}"
|
||||
OffContent="Hide labels"
|
||||
OnContent="Show Labels" />
|
||||
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Bands Section -->
|
||||
<TextBlock x:Uid="DockBandsSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<ItemsRepeater ItemsSource="{x:Bind AllDockBandItems, Mode=OneWay}">
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<DataTemplate x:DataType="dockVm:DockBandSettingsViewModel">
|
||||
<controls:SettingsCard Description="{x:Bind Description, Mode=OneWay}" Header="{x:Bind Title, Mode=OneWay}">
|
||||
<controls:SettingsCard.HeaderIcon>
|
||||
<cpControls:ContentIcon>
|
||||
<cpControls:ContentIcon.Content>
|
||||
<cpControls:IconBox
|
||||
Width="20"
|
||||
Height="20"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested}" />
|
||||
</cpControls:ContentIcon.Content>
|
||||
</cpControls:ContentIcon>
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
|
||||
<StackPanel
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<TextBlock VerticalAlignment="Center" Text="Pin to" />
|
||||
<ComboBox MinWidth="120" SelectedIndex="{x:Bind PinSideIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon Glyph="" />
|
||||
<TextBlock Text="None" />
|
||||
</StackPanel>
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<SymbolIcon Symbol="AlignLeft" />
|
||||
<TextBlock Text="Start" />
|
||||
</StackPanel>
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<SymbolIcon Symbol="AlignRight" />
|
||||
<TextBlock Text="End" />
|
||||
</StackPanel>
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
<ComboBox MinWidth="120" SelectedIndex="{x:Bind ShowLabelsIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem Content="Default" />
|
||||
<ComboBoxItem Content="Show Labels" />
|
||||
<ComboBoxItem Content="Hide Labels" />
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</controls:SettingsCard>
|
||||
</DataTemplate>
|
||||
</ItemsRepeater.ItemTemplate>
|
||||
</ItemsRepeater>
|
||||
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -1,163 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Settings;
|
||||
|
||||
public sealed partial class DockSettingsPage : Page
|
||||
{
|
||||
private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
|
||||
|
||||
private readonly SettingsViewModel viewModel;
|
||||
|
||||
public List<DockBandSettingsViewModel> AllDockBandItems => GetAllBandSettings();
|
||||
|
||||
public DockSettingsPage()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
var settings = App.Current.Services.GetService<SettingsModel>()!;
|
||||
var themeService = App.Current.Services.GetService<IThemeService>()!;
|
||||
var topLevelCommandManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
|
||||
|
||||
viewModel = new SettingsViewModel(settings, topLevelCommandManager, _mainTaskScheduler, themeService);
|
||||
|
||||
// Initialize UI state
|
||||
InitializeSettings();
|
||||
}
|
||||
|
||||
private void InitializeSettings()
|
||||
{
|
||||
// Initialize UI controls to match current settings
|
||||
DockSizeComboBox.SelectedIndex = SelectedDockSizeIndex;
|
||||
DockPositionComboBox.SelectedIndex = SelectedSideIndex;
|
||||
BackdropComboBox.SelectedIndex = SelectedBackdropIndex;
|
||||
}
|
||||
|
||||
// Property bindings for ComboBoxes
|
||||
public int SelectedDockSizeIndex
|
||||
{
|
||||
get => DockSizeToSelectedIndex(viewModel.Dock_DockSize);
|
||||
set => viewModel.Dock_DockSize = SelectedIndexToDockSize(value);
|
||||
}
|
||||
|
||||
public int SelectedSideIndex
|
||||
{
|
||||
get => SideToSelectedIndex(viewModel.Dock_Side);
|
||||
set => viewModel.Dock_Side = SelectedIndexToSide(value);
|
||||
}
|
||||
|
||||
public int SelectedBackdropIndex
|
||||
{
|
||||
get => BackdropToSelectedIndex(viewModel.Dock_Backdrop);
|
||||
set => viewModel.Dock_Backdrop = SelectedIndexToBackdrop(value);
|
||||
}
|
||||
|
||||
public bool ShowLabels
|
||||
{
|
||||
get => viewModel.Dock_ShowLabels;
|
||||
set => viewModel.Dock_ShowLabels = value;
|
||||
}
|
||||
|
||||
// Conversion methods for ComboBox bindings
|
||||
private static int DockSizeToSelectedIndex(DockSize size) => size switch
|
||||
{
|
||||
DockSize.Small => 0,
|
||||
DockSize.Medium => 1,
|
||||
DockSize.Large => 2,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
private static DockSize SelectedIndexToDockSize(int index) => index switch
|
||||
{
|
||||
0 => DockSize.Small,
|
||||
1 => DockSize.Medium,
|
||||
2 => DockSize.Large,
|
||||
_ => DockSize.Small,
|
||||
};
|
||||
|
||||
private static int SideToSelectedIndex(DockSide side) => side switch
|
||||
{
|
||||
DockSide.Left => 0,
|
||||
DockSide.Top => 1,
|
||||
DockSide.Right => 2,
|
||||
DockSide.Bottom => 3,
|
||||
_ => 1,
|
||||
};
|
||||
|
||||
private static DockSide SelectedIndexToSide(int index) => index switch
|
||||
{
|
||||
0 => DockSide.Left,
|
||||
1 => DockSide.Top,
|
||||
2 => DockSide.Right,
|
||||
3 => DockSide.Bottom,
|
||||
_ => DockSide.Top,
|
||||
};
|
||||
|
||||
private static int BackdropToSelectedIndex(DockBackdrop backdrop) => backdrop switch
|
||||
{
|
||||
DockBackdrop.Mica => 0,
|
||||
DockBackdrop.Transparent => 1,
|
||||
DockBackdrop.Acrylic => 2,
|
||||
_ => 2,
|
||||
};
|
||||
|
||||
private static DockBackdrop SelectedIndexToBackdrop(int index) => index switch
|
||||
{
|
||||
0 => DockBackdrop.Mica,
|
||||
1 => DockBackdrop.Transparent,
|
||||
2 => DockBackdrop.Acrylic,
|
||||
_ => DockBackdrop.Acrylic,
|
||||
};
|
||||
|
||||
private List<TopLevelViewModel> GetAllBands()
|
||||
{
|
||||
var allBands = new List<TopLevelViewModel>();
|
||||
|
||||
var tlcManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
|
||||
|
||||
foreach (var item in tlcManager.DockBands)
|
||||
{
|
||||
if (item.IsDockBand)
|
||||
{
|
||||
allBands.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
return allBands;
|
||||
}
|
||||
|
||||
private List<DockBandSettingsViewModel> GetAllBandSettings()
|
||||
{
|
||||
var allSettings = new List<DockBandSettingsViewModel>();
|
||||
|
||||
// var allBands = GetAllBands();
|
||||
var tlcManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
|
||||
var settingsModel = App.Current.Services.GetService<SettingsModel>()!;
|
||||
var dockViewModel = App.Current.Services.GetService<DockViewModel>()!;
|
||||
var allBands = tlcManager.DockBands;
|
||||
foreach (var band in allBands)
|
||||
{
|
||||
var setting = band.DockBandSettings;
|
||||
if (setting is not null)
|
||||
{
|
||||
var bandVm = dockViewModel.FindBandByTopLevel(band);
|
||||
allSettings.Add(new(
|
||||
dockSettingsModel: setting,
|
||||
topLevelAdapter: band,
|
||||
bandViewModel: bandVm,
|
||||
settingsModel: settingsModel
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return allSettings;
|
||||
}
|
||||
}
|
||||
@@ -85,10 +85,6 @@
|
||||
<ToggleSwitch IsOn="{x:Bind viewModel.ShowSystemTrayIcon, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_EnableDock_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind viewModel.EnableDock, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- 'For Developers' section -->
|
||||
|
||||
<TextBlock x:Uid="ForDevelopersSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
@@ -72,12 +72,6 @@
|
||||
x:Uid="Settings_GeneralPage_NavigationViewItem_Extensions"
|
||||
Icon="{ui:FontIcon Glyph=}"
|
||||
Tag="Extensions" />
|
||||
<!-- xF596 is HolePunchLandscapeTop -->
|
||||
<NavigationViewItem
|
||||
x:Name="DockSettingsPageNavItem"
|
||||
x:Uid="Settings_GeneralPage_NavigationViewItem_Dock"
|
||||
Icon="{ui:FontIcon Glyph=}"
|
||||
Tag="Dock" />
|
||||
</NavigationView.MenuItems>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
|
||||
@@ -33,9 +33,7 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
public ObservableCollection<Crumb> BreadCrumbs { get; } = [];
|
||||
|
||||
// Gets or sets optional action invoked after NavigationView is loaded.
|
||||
public Action? NavigationViewLoaded { get; set; }
|
||||
|
||||
internal string? OpenToPage { get; set; }
|
||||
public Action NavigationViewLoaded { get; set; } = () => { };
|
||||
|
||||
public SettingsWindow()
|
||||
{
|
||||
@@ -71,9 +69,7 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
Task.Delay(500).ContinueWith(_ => this.NavigationViewLoaded?.Invoke(), TaskScheduler.FromCurrentSynchronizationContext());
|
||||
|
||||
NavView.SelectedItem = NavView.MenuItems[0];
|
||||
|
||||
Navigate(OpenToPage);
|
||||
OpenToPage = null;
|
||||
Navigate("General");
|
||||
|
||||
if (sender is NavigationView navigationView)
|
||||
{
|
||||
@@ -100,36 +96,19 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
Navigate((selectedItem.Tag as string)!);
|
||||
}
|
||||
|
||||
internal void Navigate(string? page)
|
||||
private void Navigate(string page)
|
||||
{
|
||||
var pageType = page switch
|
||||
{
|
||||
null => typeof(GeneralPage),
|
||||
"General" => typeof(GeneralPage),
|
||||
"Appearance" => typeof(AppearancePage),
|
||||
"Extensions" => typeof(ExtensionsPage),
|
||||
"Dock" => typeof(DockSettingsPage),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
var actualPage = page ?? "General";
|
||||
if (pageType is not null)
|
||||
{
|
||||
// BreadCrumbs.Clear();
|
||||
// BreadCrumbs.Add(new(actualPage, actualPage));
|
||||
NavFrame.Navigate(pageType);
|
||||
|
||||
// Now, make sure to actually select the correct menu item too
|
||||
foreach (var obj in NavView.MenuItems)
|
||||
{
|
||||
if (obj is NavigationViewItem item)
|
||||
{
|
||||
if (item.Tag is string s && s == page)
|
||||
{
|
||||
NavView.SelectedItem = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,12 +261,6 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
var pageType = RS_.GetString("Settings_PageTitles_ExtensionsPage");
|
||||
BreadCrumbs.Add(new(pageType, pageType));
|
||||
}
|
||||
else if (e.SourcePageType == typeof(DockSettingsPage))
|
||||
{
|
||||
NavView.SelectedItem = DockSettingsPageNavItem;
|
||||
var pageType = RS_.GetString("Settings_PageTitles_DockPage");
|
||||
BreadCrumbs.Add(new(pageType, pageType));
|
||||
}
|
||||
else if (e.SourcePageType == typeof(ExtensionPage) && e.Parameter is ProviderSettingsViewModel vm)
|
||||
{
|
||||
NavView.SelectedItem = ExtensionPageNavItem;
|
||||
|
||||
@@ -389,9 +389,6 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="Settings_GeneralPage_NavigationViewItem_Extensions.Content" xml:space="preserve">
|
||||
<value>Extensions</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_NavigationViewItem_Dock.Content" xml:space="preserve">
|
||||
<value>Dock</value>
|
||||
</data>
|
||||
<data name="SettingsButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Open Command Palette settings</value>
|
||||
</data>
|
||||
@@ -401,12 +398,6 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="BehaviorSettingsHeader.Text" xml:space="preserve">
|
||||
<value>Behavior</value>
|
||||
</data>
|
||||
<data name="DockAppearanceSettingsHeader.Text" xml:space="preserve">
|
||||
<value>Appearance</value>
|
||||
</data>
|
||||
<data name="DockBandsSettingsHeader.Text" xml:space="preserve">
|
||||
<value>Bands</value>
|
||||
</data>
|
||||
<data name="ContextFilterBox.PlaceholderText" xml:space="preserve">
|
||||
<value>Search commands...</value>
|
||||
</data>
|
||||
@@ -421,12 +412,6 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_DisableAnimations_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Disable animations when switching between pages</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_EnableDock_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Enable dock</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_EnableDock_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Enable a toolbar with quick access to commands</value>
|
||||
</data>
|
||||
<data name="BackButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Back</value>
|
||||
@@ -631,9 +616,6 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="Settings_PageTitles_ExtensionsPage" xml:space="preserve">
|
||||
<value>Extensions</value>
|
||||
</data>
|
||||
<data name="Settings_PageTitles_DockPage" xml:space="preserve">
|
||||
<value>Dock</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_EscapeKeyBehavior_Option_DismissEmptySearchOrGoBack.Content" xml:space="preserve">
|
||||
<value>Clear search first, then go back</value>
|
||||
</data>
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<Style x:Key="TaskBarButtonStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BackgroundSizing" Value="InnerBorderEdge" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource ButtonForeground}" />
|
||||
<Setter Property="BorderBrush" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="{ThemeResource ButtonBorderThemeThickness}" />
|
||||
<Setter Property="Padding" Value="4,2,4,2" />
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
|
||||
<Setter Property="FontWeight" Value="Normal" />
|
||||
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
|
||||
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
|
||||
<Setter Property="FocusVisualMargin" Value="-3" />
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<ContentPresenter
|
||||
x:Name="ContentPresenter"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
AnimatedIcon.State="Normal"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Background="{TemplateBinding Background}"
|
||||
BackgroundSizing="{TemplateBinding BackgroundSizing}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
ContentTransitions="{TemplateBinding ContentTransitions}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}"
|
||||
Foreground="{TemplateBinding Foreground}">
|
||||
<ContentPresenter.BackgroundTransition>
|
||||
<BrushTransition Duration="0:0:0.083" />
|
||||
</ContentPresenter.BackgroundTransition>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="PointerOver">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TaskBarButtonBackgroundPointerOver}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TaskBarButtonBorderBrushPointerOver}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<!--<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonForegroundPointerOver}" />
|
||||
</ObjectAnimationUsingKeyFrames>-->
|
||||
</Storyboard>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="PointerOver" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Pressed">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TaskBarButtonBackgroundPressed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TaskBarButtonBorderBrushPressed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<!--<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonForegroundPressed}" />
|
||||
</ObjectAnimationUsingKeyFrames>-->
|
||||
</Storyboard>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="Pressed" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Disabled">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="Transparent" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="Transparent" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonForegroundDisabled}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<VisualState.Setters>
|
||||
<!-- DisabledVisual Should be handled by the control, not the animated icon. -->
|
||||
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="Normal" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</ContentPresenter>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -1,38 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<!-- For slightly adjust the LayerOnAcrylicFillColorDefault color so that the cursor of the searchbox shows -->
|
||||
<ResourceDictionary x:Key="Default">
|
||||
<SolidColorBrush x:Key="TaskBarButtonBackgroundPointerOver" Color="#0FFFFFFF" />
|
||||
<SolidColorBrush x:Key="TaskBarButtonBackgroundPressed" Color="#0BFFFFFF" />
|
||||
<LinearGradientBrush x:Key="TaskBarButtonBorderBrushPointerOver" MappingMode="Absolute" StartPoint="0,0" EndPoint="0,3">
|
||||
<LinearGradientBrush.GradientStops>
|
||||
<GradientStop Offset="0.33" Color="#0FFFFFFF" />
|
||||
<GradientStop Offset="1.0" Color="#19FFFFFF" />
|
||||
</LinearGradientBrush.GradientStops>
|
||||
</LinearGradientBrush>
|
||||
<SolidColorBrush x:Key="TaskBarButtonBorderBrushPressed" Color="#0BFFFFFF" />
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<SolidColorBrush x:Key="TaskBarButtonBackgroundPointerOver" Color="#80FFFFFF" />
|
||||
<SolidColorBrush x:Key="TaskBarButtonBackgroundPressed" Color="#4DFFFFFF" />
|
||||
<LinearGradientBrush x:Key="TaskBarButtonBorderBrushPointerOver" MappingMode="Absolute" StartPoint="0,0" EndPoint="0,3">
|
||||
<LinearGradientBrush.GradientStops>
|
||||
<GradientStop Offset="0.33" Color="#08000000" />
|
||||
<GradientStop Offset="1.0" Color="#17000000" />
|
||||
</LinearGradientBrush.GradientStops>
|
||||
</LinearGradientBrush>
|
||||
<SolidColorBrush x:Key="TaskBarButtonBorderBrushPressed" Color="#05000000" />
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="HighContrast">
|
||||
<SolidColorBrush x:Key="TaskBarButtonBackgroundPointerOver" Color="{StaticResource SystemColorHighlightTextColor}" />
|
||||
<SolidColorBrush x:Key="TaskBarButtonBackgroundPressed" Color="{StaticResource SystemColorHighlightTextColor}" />
|
||||
<SolidColorBrush x:Key="TaskBarButtonBorderBrushPointerOver" Color="{StaticResource SystemColorHighlightColor}" />
|
||||
<SolidColorBrush x:Key="TaskBarButtonBorderBrushPressed" Color="{StaticResource SystemColorHighlightTextColor}" />
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -75,8 +75,6 @@ functionality.
|
||||
- [Advanced scenarios](#advanced-scenarios)
|
||||
- [Status messages](#status-messages)
|
||||
- [Rendering of ICommandItems in Lists and Menus](#rendering-of-icommanditems-in-lists-and-menus)
|
||||
- [Addenda I: API additions (ICommandProvider2)](#addenda-i-api-additions-icommandprovider2)
|
||||
- [Addenda IV: Dock bands](#addenda-iv-dock-bands)
|
||||
- [Class diagram](#class-diagram)
|
||||
- [Future considerations](#future-considerations)
|
||||
- [Arbitrary parameters and arguments](#arbitrary-parameters-and-arguments)
|
||||
@@ -1547,13 +1545,12 @@ public class SpongebotPage : Microsoft.CommandPalette.Extensions.Toolkit.Markdow
|
||||
this.Name = "";
|
||||
this.Icon = new("https://imgflip.com/s/meme/Mocking-Spongebob.jpg");
|
||||
}
|
||||
public void IFallbackHandler.UpdateQuery(string query) {
|
||||
public void UpdateQuery(string query) {
|
||||
if (string.IsNullOrEmpty(query)) {
|
||||
this.Name = "";
|
||||
} else {
|
||||
this.Name = ConvertToAlternatingCase(query);
|
||||
}
|
||||
return Task.CompletedTask.AsAsyncCommand();
|
||||
}
|
||||
static string ConvertToAlternatingCase(string input) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
@@ -2048,87 +2045,6 @@ Fortunately, we can put all of that (`GetApiExtensionStubs`,
|
||||
developers won't have to do anything. The toolkit will just do the right thing
|
||||
for them.
|
||||
|
||||
## Addenda IV: Dock bands
|
||||
|
||||
The "dock" is another way to surface commands to the user. This is a
|
||||
toolbar-like window that can be docked to the side of the screen, or floated as
|
||||
its own window. It enables another surface for extensions to display real-time
|
||||
information and shortcuts to users.
|
||||
|
||||
Bands are powered by the same interfaces as DevPal itself. Extensions can provide
|
||||
bands via the new `DockBand` property on `ICommandProvider3`.
|
||||
|
||||
```csharp
|
||||
interface ICommandProvider3 requires ICommandProvider2
|
||||
{
|
||||
ICommandItem[] GetDockBands();
|
||||
};
|
||||
```
|
||||
|
||||
A **Dock Band** is one "strip of items" in the dock. Each band can have multiple
|
||||
items. This allows an extension to create a strip of buttons that should all be
|
||||
treated as a single unit. For example, a media player band will want probably
|
||||
four items:
|
||||
* one for the previous track
|
||||
* one for play/pause
|
||||
* one for next track
|
||||
* and one to display the album art and track title
|
||||
|
||||
`GetDockBands` returns an array of `ICommandItem`s. Each `ICommandItem`
|
||||
represents one band in the dock. These represent all of the bands that an
|
||||
extension would allow the user to add to their dock.
|
||||
|
||||
All of the `ICommandItem`s returned from `GetDockBands` **must** have a
|
||||
`Command` with a non-empty `Id` set. If the `Id` is null or empty, DevPal will
|
||||
ignore that band.
|
||||
|
||||
Bands are not automatically added to the dock. Instead, the user must choose
|
||||
which bands they want to add. This is done via the DevPal settings page.
|
||||
Furthermore, bands are not displayed in the list of commands in DevPal itself.
|
||||
This allows extension authors to create objects that are only intended for the
|
||||
dock, without cluttering up the main DevPal UI, and vice versa.
|
||||
|
||||
DevPal will then create UI in the dock for each band the user has chosen to add.
|
||||
What that looks like will depend on the `Command` in the `ICommandItem`:
|
||||
* A `IInvokableCommand` will be rendered as a single button. Think "the
|
||||
time/date" button on the taskbar, that opens the notification center.
|
||||
* A `IListPage` will be rendered as a strip of buttons, one for each `IListItem`
|
||||
in the list. Think "media controls" for a music player.
|
||||
* A `IContentPage` will be rendered as a single button. Clicking that button
|
||||
will open a flyout with that content rendered in it. Think "weather" or "news"
|
||||
flyouts.
|
||||
|
||||
If the `Command` in the `IListItem`s of a band are pages, then clicking those
|
||||
buttons will open DevPal to that page, as if it were a flyout from the dock.
|
||||
|
||||
The `.Title` property of the top-level `ICommandItem` representing the band will
|
||||
be used as the name of the band in the settings. So a media player band might
|
||||
want to set the `Title` to "Contoso Music Player", even if the individual
|
||||
buttons in the band don't show that title.
|
||||
|
||||
Users may also "pin" a top-level command from DevPal into the dock. DevPal will
|
||||
take care of creating a new band (owned by devpal) with that command in it. This
|
||||
allows users to add quick shortcuts to their favorite commands in the dock.
|
||||
Think: pinning an app, or pinning a particular GitHub query.
|
||||
|
||||
Bands are added via ID. An extension may choose to have a TopLevelCommand and a
|
||||
DockBand with the same `Id`. In this case, if the user pins the TopLevelCommand
|
||||
to the dock, DevPal will pin the band from `GetDockBands`, rather than creating
|
||||
a simple pinned command. This allows extension authors to seamlessly have a
|
||||
top-level command present a palette-specific experience, while also having a
|
||||
dock-specific experience. In our ongoing media player example, the top-level
|
||||
command might open DevPal to a full-featured music control page, while the dock
|
||||
band has simpler buttons on it (without a title/subtitle).
|
||||
|
||||
Users may choose to have:
|
||||
* the orientation of the dock: vertical or horizontal
|
||||
* the size of the dock
|
||||
* which bands are shown in the dock
|
||||
* whether the "labels" (read: `Title` & `Subtitle`) of individual bands are
|
||||
shown or hidden.
|
||||
- Dock bands will still display the `Title` & `Subtitle` of each item in the
|
||||
band as the tooltip on those items, even when the "labels" are hidden.
|
||||
|
||||
## Class diagram
|
||||
|
||||
This is a diagram attempting to show the relationships between the various types we've defined for the SDK. Some elements are omitted for clarity. (Notably, `IconData` and `IPropChanged`, which are used in many places.)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -16,8 +16,7 @@ public partial class ClipboardHistoryCommandsProvider : CommandProvider
|
||||
|
||||
public ClipboardHistoryCommandsProvider()
|
||||
{
|
||||
var page = new ClipboardHistoryListPage(_settingsManager);
|
||||
_clipboardHistoryListItem = new ListItem(page)
|
||||
_clipboardHistoryListItem = new ListItem(new ClipboardHistoryListPage(_settingsManager))
|
||||
{
|
||||
Title = Properties.Resources.list_item_title,
|
||||
Icon = Icons.ClipboardListIcon,
|
||||
@@ -25,6 +24,7 @@ public partial class ClipboardHistoryCommandsProvider : CommandProvider
|
||||
new CommandContextItem(_settingsManager.Settings.SettingsPage),
|
||||
],
|
||||
};
|
||||
|
||||
DisplayName = Properties.Resources.provider_display_name;
|
||||
Icon = Icons.ClipboardListIcon;
|
||||
Id = "Windows.ClipboardHistory";
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace CoreWidgetProvider.Widgets.Enums;
|
||||
|
||||
public enum WidgetDataState
|
||||
{
|
||||
Unknown,
|
||||
Requested, // Request is out, waiting on a response. Current data is stale.
|
||||
Okay, // Received and updated data, stable state.
|
||||
Failed, // Failed retrieving data.
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace CoreWidgetProvider.Widgets.Enums;
|
||||
|
||||
public enum WidgetPageState
|
||||
{
|
||||
Unknown,
|
||||
Configure,
|
||||
Loading,
|
||||
Content,
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace CoreWidgetProvider.Helpers;
|
||||
|
||||
internal sealed partial class CPUStats : IDisposable
|
||||
{
|
||||
// CPU counters
|
||||
private readonly PerformanceCounter _procPerf = new("Processor Information", "% Processor Utility", "_Total");
|
||||
private readonly PerformanceCounter _procPerformance = new("Processor Information", "% Processor Performance", "_Total");
|
||||
private readonly PerformanceCounter _procFrequency = new("Processor Information", "Processor Frequency", "_Total");
|
||||
private readonly Dictionary<Process, PerformanceCounter> _cpuCounters = new();
|
||||
|
||||
internal sealed class ProcessStats
|
||||
{
|
||||
public Process? Process { get; set; }
|
||||
|
||||
public float CpuUsage { get; set; }
|
||||
}
|
||||
|
||||
public float CpuUsage { get; set; }
|
||||
|
||||
public float CpuSpeed { get; set; }
|
||||
|
||||
public ProcessStats[] ProcessCPUStats { get; set; }
|
||||
|
||||
public List<float> CpuChartValues { get; set; } = new();
|
||||
|
||||
public CPUStats()
|
||||
{
|
||||
CpuUsage = 0;
|
||||
ProcessCPUStats =
|
||||
[
|
||||
new ProcessStats(),
|
||||
new ProcessStats(),
|
||||
new ProcessStats()
|
||||
];
|
||||
|
||||
InitCPUPerfCounters();
|
||||
}
|
||||
|
||||
private void InitCPUPerfCounters()
|
||||
{
|
||||
var allProcesses = Process.GetProcesses().Where(p => (long)p.MainWindowHandle != 0);
|
||||
|
||||
foreach (var process in allProcesses)
|
||||
{
|
||||
_cpuCounters.Add(process, new PerformanceCounter("Process", "% Processor Time", process.ProcessName, true));
|
||||
}
|
||||
}
|
||||
|
||||
public void GetData(bool includeTopProcesses)
|
||||
{
|
||||
var timer = Stopwatch.StartNew();
|
||||
CpuUsage = _procPerf.NextValue() / 100;
|
||||
var usageMs = timer.ElapsedMilliseconds;
|
||||
CpuSpeed = _procFrequency.NextValue() * (_procPerformance.NextValue() / 100);
|
||||
var speedMs = timer.ElapsedMilliseconds - usageMs;
|
||||
lock (CpuChartValues)
|
||||
{
|
||||
ChartHelper.AddNextChartValue(CpuUsage * 100, CpuChartValues);
|
||||
}
|
||||
|
||||
var chartMs = timer.ElapsedMilliseconds - speedMs;
|
||||
|
||||
var processCPUUsages = new Dictionary<Process, float>();
|
||||
|
||||
if (includeTopProcesses)
|
||||
{
|
||||
foreach (var processCounter in _cpuCounters)
|
||||
{
|
||||
try
|
||||
{
|
||||
// process might be terminated
|
||||
processCPUUsages.Add(processCounter.Key, processCounter.Value.NextValue() / Environment.ProcessorCount);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// _log.Information($"ProcessCounter Key {processCounter.Key} no longer exists, removing from _cpuCounters.");
|
||||
_cpuCounters.Remove(processCounter.Key);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// _log.Error(ex, "Error going through process counters.");
|
||||
}
|
||||
}
|
||||
|
||||
var cpuIndex = 0;
|
||||
foreach (var processCPUValue in processCPUUsages.OrderByDescending(x => x.Value).Take(3))
|
||||
{
|
||||
ProcessCPUStats[cpuIndex].Process = processCPUValue.Key;
|
||||
ProcessCPUStats[cpuIndex].CpuUsage = processCPUValue.Value;
|
||||
cpuIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
timer.Stop();
|
||||
var total = timer.ElapsedMilliseconds;
|
||||
var processesMs = total - chartMs;
|
||||
|
||||
// CoreLogger.LogDebug($"[{usageMs}]+[{speedMs}]+[{chartMs}]+[{processesMs}]=[{total}]");
|
||||
}
|
||||
|
||||
internal string CreateCPUImageUrl()
|
||||
{
|
||||
return ChartHelper.CreateImageUrl(CpuChartValues, ChartHelper.ChartType.CPU);
|
||||
}
|
||||
|
||||
internal string GetCpuProcessText(int cpuProcessIndex)
|
||||
{
|
||||
if (cpuProcessIndex >= ProcessCPUStats.Length)
|
||||
{
|
||||
return "no data";
|
||||
}
|
||||
|
||||
return $"{ProcessCPUStats[cpuProcessIndex].Process?.ProcessName} ({ProcessCPUStats[cpuProcessIndex].CpuUsage / 100:p})";
|
||||
}
|
||||
|
||||
internal void KillTopProcess(int cpuProcessIndex)
|
||||
{
|
||||
if (cpuProcessIndex >= ProcessCPUStats.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ProcessCPUStats[cpuProcessIndex].Process?.Kill();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_procPerf.Dispose();
|
||||
_procPerformance.Dispose();
|
||||
_procFrequency.Dispose();
|
||||
|
||||
foreach (var counter in _cpuCounters.Values)
|
||||
{
|
||||
counter.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,289 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace CoreWidgetProvider.Helpers;
|
||||
|
||||
internal sealed class ChartHelper
|
||||
{
|
||||
public enum ChartType
|
||||
{
|
||||
CPU,
|
||||
GPU,
|
||||
Mem,
|
||||
Net,
|
||||
}
|
||||
|
||||
public const int ChartHeight = 86;
|
||||
public const int ChartWidth = 268;
|
||||
|
||||
private const string LightGrayBoxStyle = "fill:none;stroke:lightgrey;stroke-width:1";
|
||||
|
||||
private const string CPULineStyle = "fill:none;stroke:rgb(57,184,227);stroke-width:1";
|
||||
private const string GPULineStyle = "fill:none;stroke:rgb(222,104,242);stroke-width:1";
|
||||
private const string MemLineStyle = "fill:none;stroke:rgb(92,158,250);stroke-width:1";
|
||||
private const string NetLineStyle = "fill:none;stroke:rgb(245,98,142);stroke-width:1";
|
||||
|
||||
private const string FillStyle = "fill:url(#gradientId);stroke:transparent";
|
||||
|
||||
private const string CPUBrushStop1Style = "stop-color:rgb(57,184,227);stop-opacity:0.4";
|
||||
private const string CPUBrushStop2Style = "stop-color:rgb(0,86,110);stop-opacity:0.25";
|
||||
|
||||
private const string GPUBrushStop1Style = "stop-color:rgb(222,104,242);stop-opacity:0.4";
|
||||
private const string GPUBrushStop2Style = "stop-color:rgb(125,0,138);stop-opacity:0.25";
|
||||
|
||||
private const string MemBrushStop1Style = "stop-color:rgb(92,158,250);stop-opacity:0.4";
|
||||
private const string MemBrushStop2Style = "stop-color:rgb(0,34,92);stop-opacity:0.25";
|
||||
|
||||
private const string NetBrushStop1Style = "stop-color:rgb(245,98,142);stop-opacity:0.4";
|
||||
private const string NetBrushStop2Style = "stop-color:rgb(130,0,47);stop-opacity:0.25";
|
||||
|
||||
private const string SvgElement = "svg";
|
||||
private const string RectElement = "rect";
|
||||
private const string PolylineElement = "polyline";
|
||||
private const string DefsElement = "defs";
|
||||
private const string LinearGradientElement = "linearGradient";
|
||||
private const string StopElement = "stop";
|
||||
|
||||
private const string HeightAttr = "height";
|
||||
private const string WidthAttr = "width";
|
||||
private const string StyleAttr = "style";
|
||||
private const string PointsAttr = "points";
|
||||
private const string OffsetAttr = "offset";
|
||||
private const string X1Attr = "x1";
|
||||
private const string X2Attr = "x2";
|
||||
private const string Y1Attr = "y1";
|
||||
private const string Y2Attr = "y2";
|
||||
private const string IdAttr = "id";
|
||||
|
||||
private const int MaxChartValues = 34;
|
||||
|
||||
public static string CreateImageUrl(List<float> chartValues, ChartType type)
|
||||
{
|
||||
var chartStr = CreateChart(chartValues, type);
|
||||
return "data:image/svg+xml;utf8," + chartStr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an SVG image for the chart.
|
||||
/// </summary>
|
||||
/// <param name="chartValues">The values to plot on the chart</param>
|
||||
/// <param name="type">The type of chart. Each chart type uses different colors.</param>
|
||||
/// <remarks>
|
||||
/// The SVG is made of three shapes: <br/>
|
||||
/// 1. A colored line, plotting the points on the graph <br/>
|
||||
/// 2. A transparent line, outlining the gradient under the graph <br/>
|
||||
/// 3. A grey box, outlining the entire image <br/>
|
||||
/// The SVG also contains a definition for the fill gradient.
|
||||
/// </remarks>
|
||||
/// <returns>A string representing the chart as an SVG image.</returns>
|
||||
public static string CreateChart(List<float> chartValues, ChartType type)
|
||||
{
|
||||
// The SVG created by this method will look similar to this:
|
||||
/*
|
||||
<svg height="102" width="264">
|
||||
<defs>
|
||||
<linearGradient x1="0%" x2="0%" y1="0%" y2="100%" id="gradientId">
|
||||
<stop offset="0%" style="stop-color:rgb(222,104,242);stop-opacity:0.4" />
|
||||
<stop offset="95%" style="stop-color:rgb(125,0,138);stop-opacity:0.25" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<polyline points="1,91 10,71 253,51 262,31 262,101 1,101" style="fill:url(#gradientId);stroke:transparent" />
|
||||
<polyline points="1,91 10,71 253,51 262,31" style="fill:none;stroke:rgb(222,104,242);stroke-width:1" />
|
||||
<rect height="102" width="264" style="fill:none;stroke:lightgrey;stroke-width:1" />
|
||||
</svg>
|
||||
*/
|
||||
|
||||
// The following code can be uncommented for testing when a static image is desired.
|
||||
/* chartValues.Clear();
|
||||
chartValues = new List<float>
|
||||
{
|
||||
10, 30, 20, 40, 30, 50, 40, 60, 50, 100,
|
||||
10, 30, 20, 40, 30, 50, 40, 60, 50, 70,
|
||||
0, 30, 20, 40, 30, 50, 40, 60, 50, 70,
|
||||
};*/
|
||||
|
||||
var chartDoc = new XDocument();
|
||||
|
||||
lock (chartValues)
|
||||
{
|
||||
var svgElement = CreateBlankSvg(ChartHeight, ChartWidth);
|
||||
|
||||
// Create the line that will show the points on the graph.
|
||||
var lineElement = new XElement(PolylineElement);
|
||||
var points = TransformPointsToLine(chartValues, out var startX, out var finalX);
|
||||
lineElement.SetAttributeValue(PointsAttr, points.ToString());
|
||||
lineElement.SetAttributeValue(StyleAttr, GetLineStyle(type));
|
||||
|
||||
// Create the line that will contain the gradient fill.
|
||||
TransformPointsToLoop(points, startX, finalX);
|
||||
var fillElement = new XElement(PolylineElement);
|
||||
fillElement.SetAttributeValue(PointsAttr, points.ToString());
|
||||
fillElement.SetAttributeValue(StyleAttr, FillStyle);
|
||||
|
||||
// Add the gradient definition and the three shapes to the svg.
|
||||
svgElement.Add(CreateGradientDefinition(type));
|
||||
svgElement.Add(fillElement);
|
||||
svgElement.Add(lineElement);
|
||||
svgElement.Add(CreateBorderBox(ChartHeight, ChartWidth));
|
||||
|
||||
chartDoc.Add(svgElement);
|
||||
}
|
||||
|
||||
return chartDoc.ToString();
|
||||
}
|
||||
|
||||
private static XElement CreateBlankSvg(int height, int width)
|
||||
{
|
||||
var svgElement = new XElement(SvgElement);
|
||||
svgElement.SetAttributeValue(HeightAttr, height);
|
||||
svgElement.SetAttributeValue(WidthAttr, width);
|
||||
return svgElement;
|
||||
}
|
||||
|
||||
private static XElement CreateGradientDefinition(ChartType type)
|
||||
{
|
||||
var defsElement = new XElement(DefsElement);
|
||||
var gradientElement = new XElement(LinearGradientElement);
|
||||
|
||||
// Vertical gradients are created when x1 and x2 are equal and y1 and y2 differ.
|
||||
gradientElement.SetAttributeValue(X1Attr, "0%");
|
||||
gradientElement.SetAttributeValue(X2Attr, "0%");
|
||||
gradientElement.SetAttributeValue(Y1Attr, "0%");
|
||||
gradientElement.SetAttributeValue(Y2Attr, "100%");
|
||||
gradientElement.SetAttributeValue(IdAttr, "gradientId");
|
||||
|
||||
string stop1Style;
|
||||
string stop2Style;
|
||||
switch (type)
|
||||
{
|
||||
case ChartType.GPU:
|
||||
stop1Style = GPUBrushStop1Style;
|
||||
stop2Style = GPUBrushStop2Style;
|
||||
break;
|
||||
case ChartType.Mem:
|
||||
stop1Style = MemBrushStop1Style;
|
||||
stop2Style = MemBrushStop2Style;
|
||||
break;
|
||||
case ChartType.Net:
|
||||
stop1Style = NetBrushStop1Style;
|
||||
stop2Style = NetBrushStop2Style;
|
||||
break;
|
||||
case ChartType.CPU:
|
||||
default:
|
||||
stop1Style = CPUBrushStop1Style;
|
||||
stop2Style = CPUBrushStop2Style;
|
||||
break;
|
||||
}
|
||||
|
||||
var stop1 = new XElement(StopElement);
|
||||
stop1.SetAttributeValue(OffsetAttr, "0%");
|
||||
stop1.SetAttributeValue(StyleAttr, stop1Style);
|
||||
|
||||
var stop2 = new XElement(StopElement);
|
||||
stop2.SetAttributeValue(OffsetAttr, "95%");
|
||||
stop2.SetAttributeValue(StyleAttr, stop2Style);
|
||||
|
||||
gradientElement.Add(stop1);
|
||||
gradientElement.Add(stop2);
|
||||
defsElement.Add(gradientElement);
|
||||
|
||||
return defsElement;
|
||||
}
|
||||
|
||||
private static XElement CreateBorderBox(int height, int width)
|
||||
{
|
||||
var boxElement = new XElement(RectElement);
|
||||
boxElement.SetAttributeValue(HeightAttr, height);
|
||||
boxElement.SetAttributeValue(WidthAttr, width);
|
||||
boxElement.SetAttributeValue(StyleAttr, LightGrayBoxStyle);
|
||||
return boxElement;
|
||||
}
|
||||
|
||||
private static string GetLineStyle(ChartType type)
|
||||
{
|
||||
var lineStyle = type switch
|
||||
{
|
||||
ChartType.CPU => CPULineStyle,
|
||||
ChartType.GPU => GPULineStyle,
|
||||
ChartType.Mem => MemLineStyle,
|
||||
ChartType.Net => NetLineStyle,
|
||||
_ => CPULineStyle,
|
||||
};
|
||||
|
||||
return lineStyle;
|
||||
}
|
||||
|
||||
private static StringBuilder TransformPointsToLine(List<float> chartValues, out int startX, out int finalX)
|
||||
{
|
||||
var points = new StringBuilder();
|
||||
|
||||
// The X value where the graph starts must be adjusted so that the graph is right-aligned.
|
||||
// The max available width of the widget is 268. Since there is a 1 px border around the chart, the width of the chart's line must be <=266.
|
||||
// To create a chart of exactly the right size, we'll have 34 points with 8 pixels in between:
|
||||
// 1 px left border + 1 px for first point + 33 segments * 8 px per segment + 1 px right border = 267 pixels total in width.
|
||||
const int pxBetweenPoints = 8;
|
||||
|
||||
// When the chart doesn't have all points yet, move the chart over to the right by increasing the starting X coordinate.
|
||||
// For a chart with only 1 point, the svg will not render a polyline.
|
||||
// For a chart with 2 points, starting X coordinate == 2 + (34 - 2) * 8 == 1 + 32 * 8 == 1 + 256 == 257
|
||||
// For a chart with 30 points, starting X coordinate == 2 + (34 - 34) * 8 == 1 + 0 * 8 == 1 + 0 == 2
|
||||
startX = 2 + ((MaxChartValues - chartValues.Count) * pxBetweenPoints);
|
||||
finalX = startX;
|
||||
|
||||
// Extend graph by one pixel to cover gap on the left when the chart is otherwise full.
|
||||
if (startX == 2)
|
||||
{
|
||||
var invertedHeight = 100 - chartValues[0];
|
||||
var finalY = (invertedHeight * (ChartHeight / 100.0)) - 1;
|
||||
points.Append(CultureInfo.InvariantCulture, $"1,{finalY} ");
|
||||
}
|
||||
|
||||
foreach (var origY in chartValues)
|
||||
{
|
||||
// We receive the height as a number up from the X axis (bottom of the chart), but we have to invert it
|
||||
// since the Y coordinate is relative to the top of the chart.
|
||||
var invertedHeight = 100 - origY;
|
||||
|
||||
// Scale the final Y to whatever the chart height is.
|
||||
var finalY = (invertedHeight * (ChartHeight / 100.0)) - 1;
|
||||
|
||||
points.Append(CultureInfo.InvariantCulture, $"{finalX},{finalY} ");
|
||||
finalX += pxBetweenPoints;
|
||||
}
|
||||
|
||||
// Remove the trailing space.
|
||||
if (points.Length > 0)
|
||||
{
|
||||
points.Remove(points.Length - 1, 1);
|
||||
finalX -= pxBetweenPoints;
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
private static void TransformPointsToLoop(StringBuilder points, int startX, int finalX)
|
||||
{
|
||||
// Close the loop.
|
||||
// Add a point at the most recent X value that corresponds with y = 0
|
||||
points.Append(CultureInfo.InvariantCulture, $" {finalX},{ChartHeight - 1}");
|
||||
|
||||
// Add a point at the start of the chart that corresponds with y = 0
|
||||
points.Append(CultureInfo.InvariantCulture, $" {startX},{ChartHeight - 1}");
|
||||
}
|
||||
|
||||
public static void AddNextChartValue(float value, List<float> chartValues)
|
||||
{
|
||||
if (chartValues.Count >= MaxChartValues)
|
||||
{
|
||||
chartValues.RemoveAt(0);
|
||||
}
|
||||
|
||||
chartValues.Add(value);
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
namespace CoreWidgetProvider.Helpers;
|
||||
|
||||
internal sealed partial class DataManager : IDisposable
|
||||
{
|
||||
private readonly SystemData _systemData;
|
||||
private readonly DataType _dataType;
|
||||
private readonly Timer _updateTimer;
|
||||
private readonly Action _updateAction;
|
||||
|
||||
private const int OneSecondInMilliseconds = 1000;
|
||||
|
||||
public DataManager(DataType type, Action updateWidget)
|
||||
{
|
||||
_systemData = new SystemData();
|
||||
_updateAction = updateWidget;
|
||||
_dataType = type;
|
||||
|
||||
_updateTimer = new Timer(OneSecondInMilliseconds);
|
||||
_updateTimer.Elapsed += UpdateTimer_Elapsed;
|
||||
_updateTimer.AutoReset = true;
|
||||
_updateTimer.Enabled = false;
|
||||
}
|
||||
|
||||
private void GetMemoryData()
|
||||
{
|
||||
lock (SystemData.MemStats)
|
||||
{
|
||||
SystemData.MemStats.GetData();
|
||||
}
|
||||
}
|
||||
|
||||
private void GetNetworkData()
|
||||
{
|
||||
lock (SystemData.NetStats)
|
||||
{
|
||||
SystemData.NetStats.GetData();
|
||||
}
|
||||
}
|
||||
|
||||
private void GetGPUData()
|
||||
{
|
||||
lock (SystemData.GPUStats)
|
||||
{
|
||||
SystemData.GPUStats.GetData();
|
||||
}
|
||||
}
|
||||
|
||||
private void GetCPUData(bool includeTopProcesses)
|
||||
{
|
||||
lock (SystemData.CpuStats)
|
||||
{
|
||||
SystemData.CpuStats.GetData(includeTopProcesses);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateTimer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
|
||||
{
|
||||
switch (_dataType)
|
||||
{
|
||||
case DataType.CPU:
|
||||
case DataType.CpuWithTopProcesses:
|
||||
{
|
||||
// CPU
|
||||
GetCPUData(_dataType == DataType.CpuWithTopProcesses);
|
||||
break;
|
||||
}
|
||||
|
||||
case DataType.GPU:
|
||||
{
|
||||
// gpu
|
||||
GetGPUData();
|
||||
break;
|
||||
}
|
||||
|
||||
case DataType.Memory:
|
||||
{
|
||||
// memory
|
||||
GetMemoryData();
|
||||
break;
|
||||
}
|
||||
|
||||
case DataType.Network:
|
||||
{
|
||||
// network
|
||||
GetNetworkData();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_updateAction?.Invoke();
|
||||
}
|
||||
|
||||
internal MemoryStats GetMemoryStats()
|
||||
{
|
||||
lock (SystemData.MemStats)
|
||||
{
|
||||
return SystemData.MemStats;
|
||||
}
|
||||
}
|
||||
|
||||
internal NetworkStats GetNetworkStats()
|
||||
{
|
||||
lock (SystemData.NetStats)
|
||||
{
|
||||
return SystemData.NetStats;
|
||||
}
|
||||
}
|
||||
|
||||
internal GPUStats GetGPUStats()
|
||||
{
|
||||
lock (SystemData.GPUStats)
|
||||
{
|
||||
return SystemData.GPUStats;
|
||||
}
|
||||
}
|
||||
|
||||
internal CPUStats GetCPUStats()
|
||||
{
|
||||
lock (SystemData.CpuStats)
|
||||
{
|
||||
return SystemData.CpuStats;
|
||||
}
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_updateTimer.Start();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_updateTimer.Stop();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_systemData.Dispose();
|
||||
_updateTimer.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace CoreWidgetProvider.Helpers;
|
||||
|
||||
public enum DataType
|
||||
{
|
||||
/// <summary>
|
||||
/// CPU related data.
|
||||
/// </summary>
|
||||
CPU,
|
||||
|
||||
/// <summary>
|
||||
/// CPU related data, including the top processes.
|
||||
/// Calculating the top processes takes a lot longer,
|
||||
/// so by default we don't.
|
||||
/// </summary>
|
||||
CpuWithTopProcesses,
|
||||
|
||||
/// <summary>
|
||||
/// Memory related data.
|
||||
/// </summary>
|
||||
Memory,
|
||||
|
||||
/// <summary>
|
||||
/// GPU related data.
|
||||
/// </summary>
|
||||
GPU,
|
||||
|
||||
/// <summary>
|
||||
/// Network related data.
|
||||
/// </summary>
|
||||
Network,
|
||||
}
|
||||
@@ -1,283 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace CoreWidgetProvider.Helpers;
|
||||
|
||||
internal sealed partial class GPUStats : IDisposable
|
||||
{
|
||||
// GPU counters
|
||||
private readonly Dictionary<int, List<PerformanceCounter>> _gpuCounters = new();
|
||||
|
||||
private readonly List<Data> _stats = new();
|
||||
|
||||
public sealed class Data
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
|
||||
public int PhysId { get; set; }
|
||||
|
||||
public float Usage { get; set; }
|
||||
|
||||
public float Temperature { get; set; }
|
||||
|
||||
public List<float> GpuChartValues { get; set; } = new();
|
||||
}
|
||||
|
||||
public GPUStats()
|
||||
{
|
||||
GetGPUPerfCounters();
|
||||
LoadGPUsFromCounters();
|
||||
}
|
||||
|
||||
public void GetGPUPerfCounters()
|
||||
{
|
||||
// There are really 4 different things we should be tracking the usage
|
||||
// of. Similar to how the instance name ends with `3D`, the following
|
||||
// suffixes are important.
|
||||
//
|
||||
// * `3D`
|
||||
// * `VideoEncode`
|
||||
// * `VideoDecode`
|
||||
// * `VideoProcessing`
|
||||
//
|
||||
// We could totally put each of those sets of counters into their own
|
||||
// set. That's what we should do, so that we can report the sum of those
|
||||
// numbers as the total utilization, and then have them broken out in
|
||||
// the card template and in the details metadata.
|
||||
_gpuCounters.Clear();
|
||||
|
||||
var pcg = new PerformanceCounterCategory("GPU Engine");
|
||||
var instanceNames = pcg.GetInstanceNames();
|
||||
|
||||
foreach (var instanceName in instanceNames)
|
||||
{
|
||||
if (!instanceName.EndsWith("3D", StringComparison.InvariantCulture))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var utilizationCounters = pcg.GetCounters(instanceName)
|
||||
.Where(x => x.CounterName.StartsWith("Utilization Percentage", StringComparison.InvariantCulture));
|
||||
|
||||
foreach (var counter in utilizationCounters)
|
||||
{
|
||||
var counterKey = counter.InstanceName;
|
||||
|
||||
// skip these values
|
||||
GetKeyValueFromCounterKey("pid", ref counterKey);
|
||||
GetKeyValueFromCounterKey("luid", ref counterKey);
|
||||
|
||||
int phys;
|
||||
var success = int.TryParse(GetKeyValueFromCounterKey("phys", ref counterKey), out phys);
|
||||
if (success)
|
||||
{
|
||||
GetKeyValueFromCounterKey("eng", ref counterKey);
|
||||
var engtype = GetKeyValueFromCounterKey("engtype", ref counterKey);
|
||||
if (engtype != "3D")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_gpuCounters.TryGetValue(phys, out var value))
|
||||
{
|
||||
value = new();
|
||||
_gpuCounters.Add(phys, value);
|
||||
}
|
||||
|
||||
value.Add(counter);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadGPUsFromCounters()
|
||||
{
|
||||
// The old dev home code tracked GPU stats by querying WMI for the list
|
||||
// of GPUs, and then matching them up with the performance counter IDs.
|
||||
//
|
||||
// We can't use WMI here, because it drags in a dependency on
|
||||
// Microsoft.Management.Infrastructure, which is not compatible with
|
||||
// AOT.
|
||||
//
|
||||
// For now, we'll just use the indicies as the GPU names.
|
||||
_stats.Clear();
|
||||
foreach (var (k, v) in _gpuCounters)
|
||||
{
|
||||
var id = k;
|
||||
var counters = v;
|
||||
_stats.Add(new Data() { PhysId = id, Name = "GPU " + id });
|
||||
}
|
||||
}
|
||||
|
||||
public void GetData()
|
||||
{
|
||||
foreach (var gpu in _stats)
|
||||
{
|
||||
List<PerformanceCounter>? counters;
|
||||
var success = _gpuCounters.TryGetValue(gpu.PhysId, out counters);
|
||||
|
||||
if (success && counters != null)
|
||||
{
|
||||
// TODO: This outer try/catch should be replaced with more secure locking around shared resources.
|
||||
try
|
||||
{
|
||||
var sum = 0.0f;
|
||||
var countersToRemove = new List<PerformanceCounter>();
|
||||
foreach (var counter in counters)
|
||||
{
|
||||
try
|
||||
{
|
||||
// NextValue() can throw an InvalidOperationException if the counter is no longer there.
|
||||
sum += counter.NextValue();
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// We can't modify the list during the loop, so save it to remove at the end.
|
||||
// _log.Information(ex, "Failed to get next value, remove");
|
||||
countersToRemove.Add(counter);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// _log.Error(ex, "Error going through process counters.");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var counter in countersToRemove)
|
||||
{
|
||||
counters.Remove(counter);
|
||||
counter.Dispose();
|
||||
}
|
||||
|
||||
gpu.Usage = sum / 100;
|
||||
lock (gpu.GpuChartValues)
|
||||
{
|
||||
ChartHelper.AddNextChartValue(sum, gpu.GpuChartValues);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// _log.Error(ex, "Error summing process counters.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal string CreateGPUImageUrl(int gpuChartIndex)
|
||||
{
|
||||
return ChartHelper.CreateImageUrl(_stats.ElementAt(gpuChartIndex).GpuChartValues, ChartHelper.ChartType.GPU);
|
||||
}
|
||||
|
||||
internal string GetGPUName(int gpuActiveIndex)
|
||||
{
|
||||
if (_stats.Count <= gpuActiveIndex)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return _stats[gpuActiveIndex].Name ?? string.Empty;
|
||||
}
|
||||
|
||||
internal int GetPrevGPUIndex(int gpuActiveIndex)
|
||||
{
|
||||
if (_stats.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (gpuActiveIndex == 0)
|
||||
{
|
||||
return _stats.Count - 1;
|
||||
}
|
||||
|
||||
return gpuActiveIndex - 1;
|
||||
}
|
||||
|
||||
internal int GetNextGPUIndex(int gpuActiveIndex)
|
||||
{
|
||||
if (_stats.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (gpuActiveIndex == _stats.Count - 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return gpuActiveIndex + 1;
|
||||
}
|
||||
|
||||
internal float GetGPUUsage(int gpuActiveIndex, string gpuActiveEngType)
|
||||
{
|
||||
if (_stats.Count <= gpuActiveIndex)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return _stats[gpuActiveIndex].Usage;
|
||||
}
|
||||
|
||||
internal string GetGPUTemperature(int gpuActiveIndex)
|
||||
{
|
||||
// MG Jan 2026: This code was lifted from the old Dev Home codebase.
|
||||
// However, the performance counters for GPU temperature are not being
|
||||
// collected. So this function always returns "--" for now.
|
||||
//
|
||||
// I have not done the code archeology to figure out why they were
|
||||
// removed.
|
||||
if (_stats.Count <= gpuActiveIndex)
|
||||
{
|
||||
return "--";
|
||||
}
|
||||
|
||||
var temperature = _stats[gpuActiveIndex].Temperature;
|
||||
if (temperature == 0)
|
||||
{
|
||||
return "--";
|
||||
}
|
||||
|
||||
return temperature.ToString("0.", CultureInfo.InvariantCulture) + " \x00B0C";
|
||||
}
|
||||
|
||||
private string GetKeyValueFromCounterKey(string key, ref string counterKey)
|
||||
{
|
||||
if (!counterKey.StartsWith(key, StringComparison.InvariantCulture))
|
||||
{
|
||||
return "error";
|
||||
}
|
||||
|
||||
counterKey = counterKey.Substring(key.Length + 1);
|
||||
if (key.Equals("engtype", StringComparison.Ordinal))
|
||||
{
|
||||
return counterKey;
|
||||
}
|
||||
|
||||
var pos = counterKey.IndexOf('_');
|
||||
if (key.Equals("luid", StringComparison.Ordinal))
|
||||
{
|
||||
pos = counterKey.IndexOf('_', pos + 1);
|
||||
}
|
||||
|
||||
var retValue = counterKey.Substring(0, pos);
|
||||
counterKey = counterKey.Substring(pos + 1);
|
||||
return retValue;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var counterPair in _gpuCounters)
|
||||
{
|
||||
foreach (var counter in counterPair.Value)
|
||||
{
|
||||
counter.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Win32;
|
||||
|
||||
namespace CoreWidgetProvider.Helpers;
|
||||
|
||||
internal sealed partial class MemoryStats : IDisposable
|
||||
{
|
||||
private readonly PerformanceCounter _memCommitted = new("Memory", "Committed Bytes", string.Empty);
|
||||
private readonly PerformanceCounter _memCached = new("Memory", "Cache Bytes", string.Empty);
|
||||
private readonly PerformanceCounter _memCommittedLimit = new("Memory", "Commit Limit", string.Empty);
|
||||
private readonly PerformanceCounter _memPoolPaged = new("Memory", "Pool Paged Bytes", string.Empty);
|
||||
private readonly PerformanceCounter _memPoolNonPaged = new("Memory", "Pool Nonpaged Bytes", string.Empty);
|
||||
|
||||
public float MemUsage
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public ulong AllMem
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public ulong UsedMem
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public ulong MemCommitted
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public ulong MemCommitLimit
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public ulong MemCached
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public ulong MemPagedPool
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public ulong MemNonPagedPool
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<float> MemChartValues { get; set; } = new();
|
||||
|
||||
public void GetData()
|
||||
{
|
||||
Windows.Win32.System.SystemInformation.MEMORYSTATUSEX memStatus = default;
|
||||
memStatus.dwLength = (uint)Marshal.SizeOf<Windows.Win32.System.SystemInformation.MEMORYSTATUSEX>();
|
||||
if (PInvoke.GlobalMemoryStatusEx(ref memStatus))
|
||||
{
|
||||
AllMem = memStatus.ullTotalPhys;
|
||||
var availableMem = memStatus.ullAvailPhys;
|
||||
UsedMem = AllMem - availableMem;
|
||||
|
||||
MemUsage = (float)UsedMem / AllMem;
|
||||
lock (MemChartValues)
|
||||
{
|
||||
ChartHelper.AddNextChartValue(MemUsage * 100, MemChartValues);
|
||||
}
|
||||
}
|
||||
|
||||
MemCached = (ulong)_memCached.NextValue();
|
||||
MemCommitted = (ulong)_memCommitted.NextValue();
|
||||
MemCommitLimit = (ulong)_memCommittedLimit.NextValue();
|
||||
MemPagedPool = (ulong)_memPoolPaged.NextValue();
|
||||
MemNonPagedPool = (ulong)_memPoolNonPaged.NextValue();
|
||||
}
|
||||
|
||||
public string CreateMemImageUrl()
|
||||
{
|
||||
return ChartHelper.CreateImageUrl(MemChartValues, ChartHelper.ChartType.Mem);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_memCommitted.Dispose();
|
||||
_memCached.Dispose();
|
||||
_memCommittedLimit.Dispose();
|
||||
_memPoolPaged.Dispose();
|
||||
_memPoolNonPaged.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace CoreWidgetProvider.Helpers;
|
||||
|
||||
internal sealed partial class NetworkStats : IDisposable
|
||||
{
|
||||
private readonly Dictionary<string, List<PerformanceCounter>> _networkCounters = new();
|
||||
|
||||
private Dictionary<string, Data> NetworkUsages { get; set; } = new();
|
||||
|
||||
private Dictionary<string, List<float>> NetChartValues { get; set; } = new();
|
||||
|
||||
public sealed class Data
|
||||
{
|
||||
public float Usage
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public float Sent
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public float Received
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
|
||||
public NetworkStats()
|
||||
{
|
||||
InitNetworkPerfCounters();
|
||||
}
|
||||
|
||||
private void InitNetworkPerfCounters()
|
||||
{
|
||||
var pcc = new PerformanceCounterCategory("Network Interface");
|
||||
var instanceNames = pcc.GetInstanceNames();
|
||||
foreach (var instanceName in instanceNames)
|
||||
{
|
||||
var instanceCounters = new List<PerformanceCounter>();
|
||||
instanceCounters.Add(new PerformanceCounter("Network Interface", "Bytes Sent/sec", instanceName));
|
||||
instanceCounters.Add(new PerformanceCounter("Network Interface", "Bytes Received/sec", instanceName));
|
||||
instanceCounters.Add(new PerformanceCounter("Network Interface", "Current Bandwidth", instanceName));
|
||||
_networkCounters.Add(instanceName, instanceCounters);
|
||||
NetChartValues.Add(instanceName, new List<float>());
|
||||
NetworkUsages.Add(instanceName, new Data());
|
||||
}
|
||||
}
|
||||
|
||||
public void GetData()
|
||||
{
|
||||
float maxUsage = 0;
|
||||
foreach (var networkCounterWithName in _networkCounters)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sent = networkCounterWithName.Value[0].NextValue();
|
||||
var received = networkCounterWithName.Value[1].NextValue();
|
||||
var bandWidth = networkCounterWithName.Value[2].NextValue();
|
||||
if (bandWidth == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var usage = 8 * (sent + received) / bandWidth;
|
||||
var name = networkCounterWithName.Key;
|
||||
NetworkUsages[name].Sent = sent;
|
||||
NetworkUsages[name].Received = received;
|
||||
NetworkUsages[name].Usage = usage;
|
||||
|
||||
var chartValues = NetChartValues[name];
|
||||
lock (chartValues)
|
||||
{
|
||||
ChartHelper.AddNextChartValue(usage * 100, chartValues);
|
||||
}
|
||||
|
||||
if (usage > maxUsage)
|
||||
{
|
||||
maxUsage = usage;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Log.Error(ex, "Error getting network data.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string CreateNetImageUrl(int netChartIndex)
|
||||
{
|
||||
return ChartHelper.CreateImageUrl(NetChartValues.ElementAt(netChartIndex).Value, ChartHelper.ChartType.Net);
|
||||
}
|
||||
|
||||
public string GetNetworkName(int networkIndex)
|
||||
{
|
||||
if (NetChartValues.Count <= networkIndex)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return NetChartValues.ElementAt(networkIndex).Key;
|
||||
}
|
||||
|
||||
public Data GetNetworkUsage(int networkIndex)
|
||||
{
|
||||
if (NetChartValues.Count <= networkIndex)
|
||||
{
|
||||
return new Data();
|
||||
}
|
||||
|
||||
var currNetworkName = NetChartValues.ElementAt(networkIndex).Key;
|
||||
if (!NetworkUsages.TryGetValue(currNetworkName, out var value))
|
||||
{
|
||||
return new Data();
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public int GetPrevNetworkIndex(int networkIndex)
|
||||
{
|
||||
if (NetChartValues.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (networkIndex == 0)
|
||||
{
|
||||
return NetChartValues.Count - 1;
|
||||
}
|
||||
|
||||
return networkIndex - 1;
|
||||
}
|
||||
|
||||
public int GetNextNetworkIndex(int networkIndex)
|
||||
{
|
||||
if (NetChartValues.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (networkIndex == NetChartValues.Count - 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return networkIndex + 1;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var counterPair in _networkCounters)
|
||||
{
|
||||
foreach (var counter in counterPair.Value)
|
||||
{
|
||||
counter.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user