mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 11:48:06 +01:00
Merge remote-tracking branch 'origin/main' into dev/snickler/net10-upgrade
This commit is contained in:
@@ -291,6 +291,7 @@
|
|||||||
"Mono.Cecil.Rocks.dll",
|
"Mono.Cecil.Rocks.dll",
|
||||||
"Newtonsoft.Json.dll",
|
"Newtonsoft.Json.dll",
|
||||||
"CommunityToolkit.WinUI.Controls.TitleBar.dll",
|
"CommunityToolkit.WinUI.Controls.TitleBar.dll",
|
||||||
|
"CommunityToolkit.WinUI.Controls.OpacityMaskView.dll",
|
||||||
|
|
||||||
"NLog.dll",
|
"NLog.dll",
|
||||||
"HtmlAgilityPack.dll",
|
"HtmlAgilityPack.dll",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<PackageVersion Include="AdaptiveCards.ObjectModel.WinUI3" Version="2.0.0-beta" />
|
<PackageVersion Include="AdaptiveCards.ObjectModel.WinUI3" Version="2.0.0-beta" />
|
||||||
<PackageVersion Include="AdaptiveCards.Rendering.WinUI3" Version="2.1.0-beta" />
|
<PackageVersion Include="AdaptiveCards.Rendering.WinUI3" Version="2.1.0-beta" />
|
||||||
<PackageVersion Include="AdaptiveCards.Templating" Version="2.0.5" />
|
<PackageVersion Include="AdaptiveCards.Templating" Version="2.0.5" />
|
||||||
|
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.OpacityMaskView" Version="0.1.251101-build.2372" />
|
||||||
<PackageVersion Include="Microsoft.Bot.AdaptiveExpressions.Core" Version="4.23.0" />
|
<PackageVersion Include="Microsoft.Bot.AdaptiveExpressions.Core" Version="4.23.0" />
|
||||||
<PackageVersion Include="Appium.WebDriver" Version="4.4.5" />
|
<PackageVersion Include="Appium.WebDriver" Version="4.4.5" />
|
||||||
<PackageVersion Include="CoenM.ImageSharp.ImageHash" Version="1.3.6" />
|
<PackageVersion Include="CoenM.ImageSharp.ImageHash" Version="1.3.6" />
|
||||||
|
|||||||
@@ -1498,6 +1498,7 @@ SOFTWARE.
|
|||||||
- CoenM.ImageSharp.ImageHash
|
- CoenM.ImageSharp.ImageHash
|
||||||
- CommunityToolkit.Common
|
- CommunityToolkit.Common
|
||||||
- CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock
|
- CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock
|
||||||
|
- CommunityToolkit.Labs.WinUI.Controls.OpacityMaskView
|
||||||
- CommunityToolkit.Mvvm
|
- CommunityToolkit.Mvvm
|
||||||
- CommunityToolkit.WinUI.Animations
|
- CommunityToolkit.WinUI.Animations
|
||||||
- CommunityToolkit.WinUI.Collections
|
- CommunityToolkit.WinUI.Collections
|
||||||
|
|||||||
240
README.md
240
README.md
@@ -51,19 +51,20 @@ But to get started quickly, choose one of the installation methods below:
|
|||||||
Go to the [PowerToys GitHub releases][github-release-link], click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer.
|
Go to the [PowerToys GitHub releases][github-release-link], click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer.
|
||||||
|
|
||||||
<!-- items that need to be updated release to release -->
|
<!-- items that need to be updated release to release -->
|
||||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.96%22
|
[github-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.95%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.95.1/PowerToysUserSetup-0.95.1-x64.exe
|
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.0/PowerToysUserSetup-0.96.0-x64.exe
|
||||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.1/PowerToysUserSetup-0.95.1-arm64.exe
|
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.0/PowerToysUserSetup-0.96.0-arm64.exe
|
||||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.1/PowerToysSetup-0.95.1-x64.exe
|
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.0/PowerToysSetup-0.96.0-x64.exe
|
||||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.1/PowerToysSetup-0.95.1-arm64.exe
|
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.0/PowerToysSetup-0.96.0-arm64.exe
|
||||||
|
|
||||||
| Description | Filename |
|
| Description | Filename |
|
||||||
|----------------|----------|
|
|----------------|----------|
|
||||||
| Per user - x64 | [PowerToysUserSetup-0.95.1-x64.exe][ptUserX64] |
|
| Per user - x64 | [PowerToysUserSetup-0.96.0-x64.exe][ptUserX64] |
|
||||||
| Per user - ARM64 | [PowerToysUserSetup-0.95.1-arm64.exe][ptUserArm64] |
|
| Per user - ARM64 | [PowerToysUserSetup-0.96.0-arm64.exe][ptUserArm64] |
|
||||||
| Machine wide - x64 | [PowerToysSetup-0.95.1-x64.exe][ptMachineX64] |
|
| Machine wide - x64 | [PowerToysSetup-0.96.0-x64.exe][ptMachineX64] |
|
||||||
| Machine wide - ARM64 | [PowerToysSetup-0.95.1-arm64.exe][ptMachineArm64] |
|
| Machine wide - ARM64 | [PowerToysSetup-0.96.0-arm64.exe][ptMachineArm64] |
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -102,156 +103,131 @@ There are [community driven install methods](./doc/unofficialInstallMethods.md)
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
## ✨ What's new
|
## ✨ What's new
|
||||||
**Version 0.95 (October 2025)**
|
**Version 0.96 (November 2025)**
|
||||||
|
|
||||||
For an in-depth look at the latest changes, visit the [Windows Command Line blog](https://aka.ms/powertoys-releaseblog).
|
For an in-depth look at the latest changes, visit the [Windows Command Line blog](https://aka.ms/powertoys-releaseblog).
|
||||||
|
|
||||||
**✨ Highlights**
|
**✨ Highlights**
|
||||||
- **NEW:** The **Light Switch** utility in PowerToys allows you to automatically switch between light and dark themes in Windows based on the time of day.
|
- Advanced Paste now supports multiple online and on-device AI model providers: Azure OpenAI, OpenAI, Google Gemini, Mistral, Foundry Local and Ollama.
|
||||||
- Command Palette delivered major search performance gains (new fuzzy matcher and smarter fallbacks) improving relevance and speed.
|
- Command Palette received extensive improvements including file search filters, better clipboard history metadata, context-menu styling, and dozens of bug fixes and enhancements.
|
||||||
- Peek can now be activated using just the Spacebar!
|
- PowerRename can now extract and use photo metadata (EXIF, XMP) in renaming patterns like `%Camera`, `%Lens`, and `%ExposureTime`.
|
||||||
- Find My Mouse added transparent spotlight with independent backdrop opacity, boosting focus and accessibility.
|
|
||||||
- Settings now lets you delete shortcuts entirely and ignore conflicts.
|
### Advanced Paste
|
||||||
- Mouse Pointer Crosshairs gained orientation options (vertical / horizontal / both) for customizable accessibility. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
- 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.
|
||||||
- PowerRename fixed enumeration counter skipping ensuring reliable batch renames. Thanks [@daverayment](https://github.com/daverayment)!
|
|
||||||
- ZoomIt restored legacy draw and snipping behaviors, and fixed recording issues, improving reliability. Thanks [@chakrik73](https://github.com/chakrik73)!
|
### 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)!
|
||||||
|
|
||||||
### Command Palette
|
### Command Palette
|
||||||
- Applied conditional margin for icon-only tags to tighten layout. Thanks [@samrueby](https://github.com/samrueby)
|
- The search field in context menus now matches the look of the Command Palette, with a smoke backdrop and improved padding.
|
||||||
- Improved the reliability of accessing Command Palette settings through PowerToys Settings and executing other x-cmdpal:// protocol commands. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
- Fallback items such as math calculations or the Run command now appear in results more quickly. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
- Enabled AOT by default for improved performance while simplifying publish configs.
|
- Ensured the command bar updates correctly after navigating to another page and commands are displayed correctly. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
- Replaced service state color dots with play/pause/stop icons for enhanced accessibility. Thanks [@samrueby](https://github.com/samrueby)
|
- The Command Palette settings page has been reorganized. Activation-key options are grouped under an expander and extension settings are framed for improved readability.
|
||||||
- Fixed filter dropdown sync and crash by binding SelectedValue and raising UI-thread notifications. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
- 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)!
|
||||||
- Ensured long links wrap correctly in details view.
|
- Press `Ctrl + ,` to open Command Palette settings from anywhere. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
- Removed animation and enforced minimum width on filter dropdown for clarity. 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)!
|
||||||
- Restored focus to More button after ESC closes context menu, improving keyboard flow. Thanks [@chatasweetie](https://github.com/chatasweetie)
|
- Fixed an issue where the search box could disappear when navigating pages. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
- Marked main and toast windows as tool windows to keep them out of Alt+Tab while preserving style. 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 AOT template and theming issues for filter separators. 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)!
|
||||||
- Introduced grid layouts (small, medium, gallery) for richer page presentation.
|
- Ensured that labels and icons of list items and menu items update when they change. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
- Materialized result lists to avoid rescoring overhead.
|
- Fixed visibility of list filters when navigating to a content page. Thanks [@DevLGuilherme](https://github.com/DevLGuilherme)!
|
||||||
- Disabled problematic selection TextToSuggest behind environment flag.
|
- Added search to the extension list and a link to extensions on the Microsoft Store. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
- Major search performance improvements (new fuzzy matcher, smarter fallbacks, fewer exceptions).
|
- Added options to open the Command Palette window at its last position or re-center it.
|
||||||
- Added context menu "Show Details" command when details pane is hidden.
|
- The Command Palette now remembers its window size after restarting.
|
||||||
- Reduced window flicker by avoiding unnecessary cloaking. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
- 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)!
|
||||||
- Restored EmptyContent rendering for blank states. Thanks [@DevLGuilherme](https://github.com/DevLGuilherme)
|
- Fixed forms and extension settings not showing on some machines due to a missing VC++ runtime.
|
||||||
- Saved new state even if prior app state file was corrupt (better resilience). Thanks [@jiripolasek](https://github.com/jiripolasek)
|
- Restored ranking of fallback commands for built-in extensions (Sleep, Shutdown, Windows settings, Web search, etc.). Thanks [@jiripolasek](https://github.com/jiripolasek).
|
||||||
- Migrated settings window to WinUI TitleBar control. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
- Improved and unified labels and texts across the application!
|
||||||
- Prevented crash on duplicate keybindings and simplified matching. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
- Maintainance: Resolved numerous build warnings in Command Palette projects; no user-visible impact. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
- Hotkeys now always respect the “Ignore shortcut in fullscreen” setting. 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)!
|
||||||
- Hid search box on content pages, improving focus and accessibility, and added Home title. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
|
||||||
- Blocked Ctrl+I from inserting stray tabs in search box.
|
|
||||||
- Logged HRESULT codes in error logs for deeper diagnostics. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
|
||||||
- Advanced font and emoji icon classification and alignment improvements. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
|
||||||
- Ensured that fallback command icons are visible on the extension settings page. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
|
||||||
- Fixed breadcrumb margin misalignment (visual polish). Thanks [@jiripolasek](https://github.com/jiripolasek)
|
|
||||||
- Truncated overly long command labels with ellipsis to prevent overflow.
|
|
||||||
- Added a setting to configure the page transition animation.
|
|
||||||
- Collection of small improvements and nits for Run Commands.
|
|
||||||
- Improved bookmarks performance and experience. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
|
||||||
- Added Ctrl+O shortcut in Clipboard History to open links directly.
|
|
||||||
- Resolved conflict with external software that blocked Command Palette from hiding.
|
|
||||||
- Updated context menu items to reflect name and icon changes, and ensured application icons are displayed correctly. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
|
||||||
- Added Alt+Home shortcut to return immediately to the Command Palette home page. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
|
||||||
- Fixed a crash when displaying code blocks in markdown on detail or content pages. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
|
||||||
- Fixed an issue where the search bar icon and title were not updated when rapidly switching pages. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
|
||||||
- Improved the appearance of the search box in the context menu.
|
|
||||||
|
|
||||||
|
|
||||||
### Command Palette Extensions
|
### Command Palette Extensions
|
||||||
- Replaced localized WebSearch setting keys with stable literals and numeric history count. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
- Bookmarks: Added hints about bookmark placeholders to the Add/Edit Bookmark form. — Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
- Enabled advanced markdown tables and emphasis extensions. 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)!
|
||||||
- Added setting to choose Clipboard History primary action (Paste vs Copy). 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)!
|
||||||
- Added actionable empty-state hints for File Search (search PC / open indexing settings). 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.
|
||||||
- Ensured all WinGet extension assets copy reliably to output. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
- 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)!
|
||||||
- Improved Run command line parsing for paths with spaces; sped up related tests.
|
- WinGet: Search performance feels more responsive because typed input is now processed via a task queue rather than complex cancellation tokens!
|
||||||
- Updated WebSearch extension icon set for enhanced clarity and contrast. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
- Window Walker: UWP apps no longer show a "not responding" label when suspended. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
- Added Terminal profile sort order setting including MRU tracking. 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)!
|
||||||
- Added Uninstall Application command (UWP direct, Win32 via Settings). Thanks [@mKpwnz](https://github.com/mKpwnz)!
|
- 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)!
|
||||||
- Deferred WinGet details loading and added timing logs.
|
|
||||||
- Removed LINQ from All Apps extension for performance.
|
|
||||||
- Added standardized key chord system + shortcuts to File Search commands. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
|
||||||
- Added Terminal channel filter & remembered selection option. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
|
||||||
- Enabled loading local/data/app images in markdown with sizing hints. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
|
||||||
- Added external extension reload via x-cmdpal://reload (configurable). Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
|
||||||
- Instant WebSearch history updates with in-memory store & events. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
|
||||||
- Added keep-after-paste option and safe delete with confirmation for Clipboard History. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
- Replaced custom window chrome with WinUI TitleBar for cleaner, maintainable Environment Variables UI.
|
|
||||||
|
|
||||||
### File Locksmith
|
|
||||||
- Adopted WinUI TitleBar to simplify window chrome while preserving appearance.
|
|
||||||
|
|
||||||
### Find My Mouse
|
### Find My Mouse
|
||||||
- Added transparent spotlight support with separate backdrop opacity; migrated to Windows App SDK composition APIs.
|
- Activating Find My Mouse no longer makes the cursor change to the busy (hourglass) icon or steals focus from your active application.
|
||||||
|
|
||||||
### Hosts File Editor
|
### Hosts File Editor
|
||||||
- Migrated to native WinUI TitleBar for cleaner, maintainable window chrome.
|
- Added customizable backup settings allowing users to configure backup frequency, location, and auto-deletion policies. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||||
|
|
||||||
|
### Image Resizer
|
||||||
|
- Fixed settings consistency during batch resize operations by capturing settings once before processing. Thanks [@daverayment](https://github.com/daverayment)!
|
||||||
|
|
||||||
### Light Switch
|
### Light Switch
|
||||||
- Introduced as a brand-new PowerToy module.
|
- Introduced new UI to allow users to manually enter their latitude and longitude in Sunrise to Sunset mode.
|
||||||
- Automatically switches between light and dark themes.
|
- Refactored service with cleaner state management for stability.
|
||||||
- Supports time-based scheduling or location-based sunrise/sunset switching.
|
- Removed logs from every tick, only logging key events to largely reduce log size.
|
||||||
- Supports using a keyboard shortcut to force a change.
|
|
||||||
- Supports filtering changes for Apps and/or System Theme.
|
|
||||||
|
|
||||||
### Mouse Pointer Crosshairs
|
### Mouse Pointer Crosshairs
|
||||||
- Added Esc key to cancel active gliding cursor sequence. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
- Enabled switching between Mouse Pointer Crosshairs and Gliding Cursor modes. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||||
- Added orientation option (vertical / horizontal / both) for crosshairs customization. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
|
||||||
|
|
||||||
### Mouse Without Borders
|
### Mouse Without Borders
|
||||||
- Continued Common class refactor (part 5/7) by extracting clipboard and init/cleanup logic into focused classes. Thanks [@mikeclayton](https://github.com/mikeclayton)!
|
- Added horizontal scrolling support. Thanks [@MasonBergstrom](https://github.com/MasonBergstrom)!
|
||||||
|
|
||||||
- Fix connection failures caused by conflicting MachineId across machines. Thanks [@noraa-junker](https://github.com/noraa-junker) for troubleshooting!
|
|
||||||
|
|
||||||
### Peek
|
### Peek
|
||||||
- Added the option to activate Peek with just the Spacebar.
|
- 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)!
|
||||||
|
|
||||||
### PowerRename
|
### PowerRename
|
||||||
- Fixed enumeration counter skipping when regex replacement equals original filename (counters now advance reliably). Thanks [@daverayment](https://github.com/daverayment)!
|
- 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).
|
||||||
|
|
||||||
### Quick Accent
|
### PowerToys Run
|
||||||
- Expanded Welsh layout with acute, grave, and dieresis variants for vowels (consistent ordering). Thanks [@PesBandi](https://github.com/PesBandi)!
|
- 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)!
|
||||||
|
|
||||||
### Registry Preview
|
### Quick Accent
|
||||||
- Migrated to native TitleBar and AppWindow APIs for cleaner window chrome.
|
- Added diameter symbol (⌀) for Shift+O in Special Characters mode, thanks to [@anselumjuju](https://github.com/anselumjuju)!
|
||||||
|
|
||||||
### Screen Ruler
|
### Zoomit
|
||||||
- Fixed ARM64 crash by aligning cursor position structure to 8-byte boundary.
|
- 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)!
|
||||||
|
|
||||||
### Settings
|
### Settings
|
||||||
- Added ability to ignore specific hotkey conflicts to reduce noise.
|
- Fixed title bar overlapping issue at smaller window sizes.
|
||||||
- Stopped creating backup directory during dry-run status checks (cleaner first-run).
|
- Refined shortcut control visual design with improved consistency and spacing.
|
||||||
- Standardized casing and localization for ZoomIt and modules header.
|
- Added dashboard utilities sorting by name or status.
|
||||||
- Improved search results page accessibility and conditional module grouping.
|
- 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)!
|
||||||
|
|
||||||
### ZoomIt
|
### Development
|
||||||
- Updated resource file to reflect standalone v9.01 and current copyright year. Thanks [@foxmsft](https://github.com/foxmsft)!
|
- Fixed accessibility by associating controls with labels for screen readers.
|
||||||
- Restored legacy draw/snipping behaviors and fixed recording race conditions. Thanks [@chakrik73](https://github.com/chakrik73)!
|
- Added accessible name to Shortcut Conflicts button for screen readers.
|
||||||
- Added smooth image option for improved zoom quality using GDI+ for static zoom and Magnifier API for live zoom. Thanks [@markrussinovich](https://github.com/markrussinovich)!
|
- 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.
|
||||||
### Documentation
|
- Configured build agents to use larger P: drive for release builds to address disk space constraints.
|
||||||
- New Microsoft Learn documentation for the Light Switch module.
|
- Enhanced DSC v3 support by organizing resource manifests in a dedicated subfolder with PATH configuration.
|
||||||
- New dev docs for the Light Switch module.
|
- 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)!
|
||||||
### Development (Area-Build & Area-Tests)
|
- Fixed spell check dictionary entries for consistency.
|
||||||
- Allowed debug launches to continue when modules fail to load, speeding developer iteration.
|
- Restored accidentally deleted NuGet configuration file for Command Palette extensions.
|
||||||
- Fixed spell checker dictionary entry (advapi) to eliminate false error.
|
- Fixed package identity build by updating AppxManifest entry points to use PowerShell Core.
|
||||||
- Added VS Code development guide and launch configs to streamline cross-editor workflows.
|
- Optimized CI pipeline by replacing file copy operations with hard links and moves, reducing build time and disk usage by 10-15GB.
|
||||||
- Upgraded Windows App SDK and related dependencies to 1.8 for newer platform features.
|
- Updated Copilot guidance and PR prompt workflow.
|
||||||
- Rewrote YAML comment to resolve new spell checker forbidden pattern. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
- Included high-volume bugs in issue template header. Thanks [@daverayment](https://github.com/daverayment)!
|
||||||
- Corrected solution structure by returning misplaced Common project, reducing build confusion.
|
- Fixed incorrect HRESULT logging for inner exceptions. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
- Modernized build scripts with shared helpers and VS environment autodetection for simpler CLI builds.
|
- Introduced shared sparse package identity for PowerToys Win32 components to enable access to Windows platform APIs.
|
||||||
- Standardized build scripts and platform detection to improve reliability and reuse.
|
- Consolidated installer builds to produce both machine and user installers simultaneously, reducing build time and complexity.
|
||||||
- Added missing Command Palette version bump to align module release cadence.
|
- Migrated exclusively to WiX v5 installer infrastructure, removing legacy WiX v3 support.
|
||||||
- Added EXECUTEDEFAULT term to dictionary to prevent regression build failures. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
- Temporarily removed PowerToys installer path from PATH environment variable to prevent application crashes.
|
||||||
- Introduced nightly pre-warm pipeline and configurable MSBuild cache mode to improve CI performance.
|
- Added complete OCR UI test coverage with automated tests for activation, settings, language selection, and text extraction.
|
||||||
- Resolved CI forbidden pattern spelling complaint to keep pipelines green.
|
- Fixed test input for drive path normalization in bookmark resolver unit tests.
|
||||||
- Added AI contributor instruction set to clarify code area expectations.
|
- Fixed Peek UI tests by restoring Ctrl+Space activation shortcut for test scenarios.
|
||||||
- Added accessibility IDs to settings and FancyZones toggles, stabilizing UI tests.
|
- Hided apps in PowerToys.SpareApps package from Start Menu. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
- Added automatic log collection on UI test failures to speed root cause analysis.
|
|
||||||
- Stabilized Mouse Utils tests by switching to AccessibilityId selectors.
|
|
||||||
- Added Screen Ruler UI test coverage to validate core measurement workflows.
|
|
||||||
|
|
||||||
## 🛣️ Roadmap
|
## 🛣️ 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 – 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]!
|
||||||
|
|||||||
@@ -33,8 +33,11 @@ The **Light Switch** module lets users automatically transition between light an
|
|||||||
|
|
||||||
> **Note:** Using the shortcut overrides the current schedule until the next transition event.
|
> **Note:** Using the shortcut overrides the current schedule until the next transition event.
|
||||||
|
|
||||||
* **LightSwitchService**
|
* **LightSwitchService.cpp**
|
||||||
Reads settings and applies theming. Runs a check every minute to ensure the state is correct.
|
is the heart beat of the module. Controls ticking every minute and depending on user actions (manual override, settings changing, etc) triggers the state manager to perform the corresponding operation.
|
||||||
|
|
||||||
|
* **LightSwitchStateManager.cpp**
|
||||||
|
handles updating the state based on the signals sent by LightSwitchService.
|
||||||
|
|
||||||
* **SettingsXAML/LightSwitch**
|
* **SettingsXAML/LightSwitch**
|
||||||
Provides the settings UI for configuring schedules, syncing location, and customizing shortcuts.
|
Provides the settings UI for configuring schedules, syncing location, and customizing shortcuts.
|
||||||
|
|||||||
@@ -42,7 +42,8 @@
|
|||||||
Description="PowerToys OCR Module"
|
Description="PowerToys OCR Module"
|
||||||
BackgroundColor="transparent"
|
BackgroundColor="transparent"
|
||||||
Square150x150Logo="Images\Square150x150Logo.png"
|
Square150x150Logo="Images\Square150x150Logo.png"
|
||||||
Square44x44Logo="Images\Square44x44Logo.png">
|
Square44x44Logo="Images\Square44x44Logo.png"
|
||||||
|
AppListEntry="none">
|
||||||
</uap:VisualElements>
|
</uap:VisualElements>
|
||||||
</Application>
|
</Application>
|
||||||
<Application Id="PowerToys.SettingsUI" Executable="WinUI3Apps\PowerToys.Settings.exe" EntryPoint="Windows.FullTrustApplication">
|
<Application Id="PowerToys.SettingsUI" Executable="WinUI3Apps\PowerToys.Settings.exe" EntryPoint="Windows.FullTrustApplication">
|
||||||
@@ -51,7 +52,8 @@
|
|||||||
Description="PowerToys Settings UI"
|
Description="PowerToys Settings UI"
|
||||||
BackgroundColor="transparent"
|
BackgroundColor="transparent"
|
||||||
Square150x150Logo="Images\Square150x150Logo.png"
|
Square150x150Logo="Images\Square150x150Logo.png"
|
||||||
Square44x44Logo="Images\Square44x44Logo.png">
|
Square44x44Logo="Images\Square44x44Logo.png"
|
||||||
|
AppListEntry="none">
|
||||||
</uap:VisualElements>
|
</uap:VisualElements>
|
||||||
</Application>
|
</Application>
|
||||||
<Application Id="PowerToys.ImageResizerUI" Executable="WinUI3Apps\PowerToys.ImageResizer.exe" EntryPoint="Windows.FullTrustApplication">
|
<Application Id="PowerToys.ImageResizerUI" Executable="WinUI3Apps\PowerToys.ImageResizer.exe" EntryPoint="Windows.FullTrustApplication">
|
||||||
@@ -60,7 +62,8 @@
|
|||||||
Description="PowerToys Image Resizer UI"
|
Description="PowerToys Image Resizer UI"
|
||||||
BackgroundColor="transparent"
|
BackgroundColor="transparent"
|
||||||
Square150x150Logo="Images\Square150x150Logo.png"
|
Square150x150Logo="Images\Square150x150Logo.png"
|
||||||
Square44x44Logo="Images\Square44x44Logo.png">
|
Square44x44Logo="Images\Square44x44Logo.png"
|
||||||
|
AppListEntry="none">
|
||||||
</uap:VisualElements>
|
</uap:VisualElements>
|
||||||
</Application>
|
</Application>
|
||||||
</Applications>
|
</Applications>
|
||||||
|
|||||||
@@ -10,6 +10,23 @@ namespace LanguageModelProvider.FoundryLocal;
|
|||||||
internal sealed class FoundryClient
|
internal sealed class FoundryClient
|
||||||
{
|
{
|
||||||
public static async Task<FoundryClient?> CreateAsync()
|
public static async Task<FoundryClient?> CreateAsync()
|
||||||
|
{
|
||||||
|
// First attempt with current environment
|
||||||
|
var client = await TryCreateClientAsync().ConfigureAwait(false);
|
||||||
|
if (client != null)
|
||||||
|
{
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If failed, refresh PATH from registry and retry once
|
||||||
|
// This handles cases where PowerToys was launched by MSI installer.
|
||||||
|
Logger.LogInfo("[FoundryClient] First attempt failed, refreshing PATH and retrying");
|
||||||
|
RefreshEnvironmentPath();
|
||||||
|
|
||||||
|
return await TryCreateClientAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<FoundryClient?> TryCreateClientAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -168,8 +185,6 @@ internal sealed class FoundryClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> EnsureModelLoaded(string modelId)
|
public async Task<bool> EnsureModelLoaded(string modelId)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
Logger.LogInfo($"[FoundryClient] EnsureModelLoaded called with: {modelId}");
|
Logger.LogInfo($"[FoundryClient] EnsureModelLoaded called with: {modelId}");
|
||||||
|
|
||||||
@@ -180,16 +195,6 @@ internal sealed class FoundryClient
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if model exists in cache
|
|
||||||
var cachedModels = await ListCachedModels().ConfigureAwait(false);
|
|
||||||
Logger.LogInfo($"[FoundryClient] Cached models: {string.Join(", ", cachedModels.Select(m => m.Name))}");
|
|
||||||
|
|
||||||
if (!cachedModels.Any(m => m.Name == modelId))
|
|
||||||
{
|
|
||||||
Logger.LogWarning($"[FoundryClient] Model not found in cache: {modelId}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the model
|
// Load the model
|
||||||
Logger.LogInfo($"[FoundryClient] Loading model: {modelId}");
|
Logger.LogInfo($"[FoundryClient] Loading model: {modelId}");
|
||||||
await _foundryManager.LoadModelAsync(modelId).ConfigureAwait(false);
|
await _foundryManager.LoadModelAsync(modelId).ConfigureAwait(false);
|
||||||
@@ -199,12 +204,6 @@ internal sealed class FoundryClient
|
|||||||
Logger.LogInfo($"[FoundryClient] Model load result: {loaded}");
|
Logger.LogInfo($"[FoundryClient] Model load result: {loaded}");
|
||||||
return loaded;
|
return loaded;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError($"[FoundryClient] EnsureModelLoaded exception: {ex.Message}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task EnsureRunning()
|
public async Task EnsureRunning()
|
||||||
{
|
{
|
||||||
@@ -213,4 +212,68 @@ internal sealed class FoundryClient
|
|||||||
await _foundryManager.StartServiceAsync();
|
await _foundryManager.StartServiceAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Refreshes the PATH environment variable from the system registry.
|
||||||
|
/// This is necessary when tools are installed while PowerToys is running,
|
||||||
|
/// as the installer updates the system PATH but running processes don't see the change.
|
||||||
|
/// </summary>
|
||||||
|
private static void RefreshEnvironmentPath()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Logger.LogInfo("[FoundryClient] Refreshing PATH environment variable from system");
|
||||||
|
|
||||||
|
var currentPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Process) ?? string.Empty;
|
||||||
|
var machinePath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Machine) ?? string.Empty;
|
||||||
|
var userPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User) ?? string.Empty;
|
||||||
|
|
||||||
|
var pathsToAdd = new List<string>();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(currentPath))
|
||||||
|
{
|
||||||
|
pathsToAdd.AddRange(currentPath.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(userPath))
|
||||||
|
{
|
||||||
|
var userPaths = userPath.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
foreach (var path in userPaths)
|
||||||
|
{
|
||||||
|
if (!pathsToAdd.Contains(path, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
pathsToAdd.Add(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(machinePath))
|
||||||
|
{
|
||||||
|
var machinePaths = machinePath.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
foreach (var path in machinePaths)
|
||||||
|
{
|
||||||
|
if (!pathsToAdd.Contains(path, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
pathsToAdd.Add(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var newPath = string.Join(Path.PathSeparator.ToString(), pathsToAdd);
|
||||||
|
|
||||||
|
if (currentPath != newPath)
|
||||||
|
{
|
||||||
|
Logger.LogInfo("[FoundryClient] Updating process PATH with latest system values");
|
||||||
|
Environment.SetEnvironmentVariable("PATH", newPath, EnvironmentVariableTarget.Process);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogInfo("[FoundryClient] PATH is already up to date");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"[FoundryClient] Failed to refresh PATH: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ namespace LanguageModelProvider;
|
|||||||
|
|
||||||
public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
||||||
{
|
{
|
||||||
private IEnumerable<ModelDetails>? _downloadedModels;
|
|
||||||
private FoundryClient? _foundryClient;
|
private FoundryClient? _foundryClient;
|
||||||
|
private IEnumerable<FoundryCatalogModel>? _catalogModels;
|
||||||
private string? _serviceUrl;
|
private string? _serviceUrl;
|
||||||
|
|
||||||
public static FoundryLocalModelProvider Instance { get; } = new();
|
public static FoundryLocalModelProvider Instance { get; } = new();
|
||||||
@@ -23,23 +23,9 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
|||||||
public string ProviderDescription => "The model will run locally via Foundry Local";
|
public string ProviderDescription => "The model will run locally via Foundry Local";
|
||||||
|
|
||||||
public IChatClient? GetIChatClient(string modelId)
|
public IChatClient? GetIChatClient(string modelId)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
Logger.LogInfo($"[FoundryLocal] GetIChatClient called with url: {modelId}");
|
Logger.LogInfo($"[FoundryLocal] GetIChatClient called with url: {modelId}");
|
||||||
InitializeAsync().GetAwaiter().GetResult();
|
InitializeAsync().GetAwaiter().GetResult();
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError($"[FoundryLocal] Failed to initialize: {ex.Message}");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(_serviceUrl) || _foundryClient == null)
|
|
||||||
{
|
|
||||||
Logger.LogError("[FoundryLocal] Service URL or manager is null");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(modelId))
|
if (string.IsNullOrWhiteSpace(modelId))
|
||||||
{
|
{
|
||||||
@@ -47,39 +33,38 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the model is loaded before returning chat client
|
// Check if model is in catalog
|
||||||
try
|
var isInCatalog = _catalogModels?.Any(m => m.Name == modelId) ?? false;
|
||||||
|
if (!isInCatalog)
|
||||||
{
|
{
|
||||||
var isLoaded = _foundryClient.EnsureModelLoaded(modelId).GetAwaiter().GetResult();
|
var errorMessage = $"{modelId} is not supported in Foundry Local. Please configure supported models in Settings.";
|
||||||
|
Logger.LogError($"[FoundryLocal] {errorMessage}");
|
||||||
|
throw new InvalidOperationException(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the model is loaded before returning chat client
|
||||||
|
var isLoaded = _foundryClient!.EnsureModelLoaded(modelId).GetAwaiter().GetResult();
|
||||||
if (!isLoaded)
|
if (!isLoaded)
|
||||||
{
|
{
|
||||||
Logger.LogError($"[FoundryLocal] Failed to load model: {modelId}");
|
Logger.LogError($"[FoundryLocal] Failed to load model: {modelId}");
|
||||||
return null;
|
throw new InvalidOperationException($"Failed to load the model '{modelId}'.");
|
||||||
}
|
|
||||||
|
|
||||||
Logger.LogInfo($"[FoundryLocal] Model is loaded: {modelId}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError($"[FoundryLocal] Exception ensuring model loaded: {ex.Message}");
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use ServiceUri instead of Endpoint since Endpoint already includes /v1
|
// Use ServiceUri instead of Endpoint since Endpoint already includes /v1
|
||||||
var baseUri = _foundryClient.GetServiceUri();
|
var baseUri = _foundryClient.GetServiceUri();
|
||||||
if (baseUri == null)
|
if (baseUri == null)
|
||||||
{
|
{
|
||||||
Logger.LogError("[FoundryLocal] Service URI is null");
|
const string message = "Foundry Local service URL is not available. Please make sure Foundry Local is installed and running.";
|
||||||
return null;
|
Logger.LogError($"[FoundryLocal] {message}");
|
||||||
|
throw new InvalidOperationException(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
var endpointUri = new Uri($"{baseUri.ToString().TrimEnd('/')}/v1");
|
var endpointUri = new Uri($"{baseUri.ToString().TrimEnd('/')}/v1");
|
||||||
Logger.LogInfo($"[FoundryLocal] Creating OpenAI client with endpoint: {endpointUri}");
|
Logger.LogInfo($"[FoundryLocal] Creating OpenAI client with endpoint: {endpointUri}");
|
||||||
Logger.LogInfo($"[FoundryLocal] Model ID for chat client: {modelId}");
|
|
||||||
|
|
||||||
return new OpenAIClient(
|
return new OpenAIClient(
|
||||||
new ApiKeyCredential("none"),
|
new ApiKeyCredential("none"),
|
||||||
new OpenAIClientOptions { Endpoint = endpointUri })
|
new OpenAIClientOptions { Endpoint = endpointUri, NetworkTimeout = TimeSpan.FromMinutes(5) })
|
||||||
.GetChatClient(modelId)
|
.GetChatClient(modelId)
|
||||||
.AsIChatClient();
|
.AsIChatClient();
|
||||||
}
|
}
|
||||||
@@ -105,49 +90,16 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
|||||||
return $"new OpenAIClient(new ApiKeyCredential(\"none\"), new OpenAIClientOptions{{ Endpoint = new Uri(\"{_serviceUrl}/v1\") }}).GetChatClient(\"{modelId}\").AsIChatClient()";
|
return $"new OpenAIClient(new ApiKeyCredential(\"none\"), new OpenAIClientOptions{{ Endpoint = new Uri(\"{_serviceUrl}/v1\") }}).GetChatClient(\"{modelId}\").AsIChatClient()";
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<ModelDetails>> GetModelsAsync(bool ignoreCached = false, CancellationToken cancelationToken = default)
|
public async Task<IEnumerable<ModelDetails>> GetModelsAsync(CancellationToken cancelationToken = default)
|
||||||
{
|
{
|
||||||
if (ignoreCached)
|
|
||||||
{
|
|
||||||
Logger.LogInfo("[FoundryLocal] Ignoring cached models, resetting");
|
|
||||||
Reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
await InitializeAsync(cancelationToken);
|
await InitializeAsync(cancelationToken);
|
||||||
|
|
||||||
Logger.LogInfo($"[FoundryLocal] Returning {_downloadedModels?.Count() ?? 0} downloaded models");
|
|
||||||
return _downloadedModels ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Reset()
|
|
||||||
{
|
|
||||||
_downloadedModels = null;
|
|
||||||
_ = InitializeAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task InitializeAsync(CancellationToken cancelationToken = default)
|
|
||||||
{
|
|
||||||
if (_foundryClient != null && _downloadedModels != null && _downloadedModels.Any())
|
|
||||||
{
|
|
||||||
await _foundryClient.EnsureRunning().ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.LogInfo("[FoundryLocal] Initializing provider");
|
|
||||||
_foundryClient ??= await FoundryClient.CreateAsync();
|
|
||||||
|
|
||||||
if (_foundryClient == null)
|
if (_foundryClient == null)
|
||||||
{
|
{
|
||||||
Logger.LogError("[FoundryLocal] Failed to create Foundry client");
|
return Array.Empty<ModelDetails>();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_serviceUrl ??= await _foundryClient.GetServiceUrl();
|
|
||||||
Logger.LogInfo($"[FoundryLocal] Service URL: {_serviceUrl}");
|
|
||||||
|
|
||||||
var cachedModels = await _foundryClient.ListCachedModels();
|
var cachedModels = await _foundryClient.ListCachedModels();
|
||||||
Logger.LogInfo($"[FoundryLocal] Found {cachedModels.Count} cached models");
|
|
||||||
|
|
||||||
List<ModelDetails> downloadedModels = [];
|
List<ModelDetails> downloadedModels = [];
|
||||||
|
|
||||||
foreach (var model in cachedModels)
|
foreach (var model in cachedModels)
|
||||||
@@ -160,13 +112,37 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
|||||||
Url = $"fl://{model.Name}",
|
Url = $"fl://{model.Name}",
|
||||||
Description = $"{model.Name} running locally with Foundry Local",
|
Description = $"{model.Name} running locally with Foundry Local",
|
||||||
HardwareAccelerators = [HardwareAccelerator.FOUNDRYLOCAL],
|
HardwareAccelerators = [HardwareAccelerator.FOUNDRYLOCAL],
|
||||||
SupportedOnQualcomm = true,
|
|
||||||
ProviderModelDetails = model,
|
ProviderModelDetails = model,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_downloadedModels = downloadedModels;
|
return downloadedModels;
|
||||||
Logger.LogInfo($"[FoundryLocal] Initialization complete. Total downloaded models: {downloadedModels.Count}");
|
}
|
||||||
|
|
||||||
|
private async Task InitializeAsync(CancellationToken cancelationToken = default)
|
||||||
|
{
|
||||||
|
if (_foundryClient != null && _catalogModels != null && _catalogModels.Any())
|
||||||
|
{
|
||||||
|
await _foundryClient.EnsureRunning().ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogInfo("[FoundryLocal] Initializing provider");
|
||||||
|
_foundryClient ??= await FoundryClient.CreateAsync();
|
||||||
|
|
||||||
|
if (_foundryClient == null)
|
||||||
|
{
|
||||||
|
const string message = "Foundry Local client could not be created. Please make sure Foundry Local is installed and running.";
|
||||||
|
Logger.LogError($"[FoundryLocal] {message}");
|
||||||
|
throw new InvalidOperationException(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
_serviceUrl ??= await _foundryClient.GetServiceUrl();
|
||||||
|
Logger.LogInfo($"[FoundryLocal] Service URL: {_serviceUrl}");
|
||||||
|
|
||||||
|
var catalogModels = await _foundryClient.ListCatalogModels();
|
||||||
|
Logger.LogInfo($"[FoundryLocal] Found {catalogModels.Count} catalog models");
|
||||||
|
_catalogModels = catalogModels;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> IsAvailable()
|
public async Task<bool> IsAvailable()
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ public interface ILanguageModelProvider
|
|||||||
|
|
||||||
string ProviderDescription { get; }
|
string ProviderDescription { get; }
|
||||||
|
|
||||||
Task<IEnumerable<ModelDetails>> GetModelsAsync(bool ignoreCached = false, CancellationToken cancelationToken = default);
|
Task<IEnumerable<ModelDetails>> GetModelsAsync(CancellationToken cancelationToken = default);
|
||||||
|
|
||||||
IChatClient? GetIChatClient(string modelId);
|
IChatClient? GetIChatClient(string modelId);
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ public class ModelDetails
|
|||||||
|
|
||||||
public List<HardwareAccelerator> HardwareAccelerators { get; set; } = [];
|
public List<HardwareAccelerator> HardwareAccelerators { get; set; } = [];
|
||||||
|
|
||||||
public bool SupportedOnQualcomm { get; set; }
|
|
||||||
|
|
||||||
public string License { get; set; } = string.Empty;
|
public string License { get; set; } = string.Empty;
|
||||||
|
|
||||||
public object? ProviderModelDetails { get; set; }
|
public object? ProviderModelDetails { get; set; }
|
||||||
|
|||||||
@@ -558,7 +558,7 @@
|
|||||||
<TextBlock
|
<TextBlock
|
||||||
x:Uid="AIProvidersFlyoutHeader"
|
x:Uid="AIProvidersFlyoutHeader"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Style="{StaticResource BodyStrongTextBlockStyle}" />
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||||
<ListView
|
<ListView
|
||||||
x:Name="AIProviderListView"
|
x:Name="AIProviderListView"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
|
|||||||
@@ -299,7 +299,8 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</controls:PromptBox.Footer>
|
</controls:PromptBox.Footer>
|
||||||
</controls:PromptBox>
|
</controls:PromptBox>
|
||||||
<Grid Grid.Row="2" RowSpacing="4">
|
<ScrollViewer Grid.Row="2">
|
||||||
|
<Grid RowSpacing="4">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="{x:Bind ViewModel.StandardPasteFormats.Count, Mode=OneWay, Converter={StaticResource standardPasteFormatsToHeightConverter}}" />
|
<RowDefinition Height="{x:Bind ViewModel.StandardPasteFormats.Count, Mode=OneWay, Converter={StaticResource standardPasteFormatsToHeightConverter}}" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
@@ -316,8 +317,8 @@
|
|||||||
ItemContainerTransitions="{x:Null}"
|
ItemContainerTransitions="{x:Null}"
|
||||||
ItemTemplateSelector="{StaticResource PasteFormatTemplateSelector}"
|
ItemTemplateSelector="{StaticResource PasteFormatTemplateSelector}"
|
||||||
ItemsSource="{x:Bind ViewModel.StandardPasteFormats, Mode=OneWay}"
|
ItemsSource="{x:Bind ViewModel.StandardPasteFormats, Mode=OneWay}"
|
||||||
ScrollViewer.VerticalScrollBarVisibility="Visible"
|
ScrollViewer.VerticalScrollBarVisibility="Disabled"
|
||||||
ScrollViewer.VerticalScrollMode="Auto"
|
ScrollViewer.VerticalScrollMode="Disabled"
|
||||||
SelectionMode="None"
|
SelectionMode="None"
|
||||||
TabIndex="1" />
|
TabIndex="1" />
|
||||||
<Rectangle
|
<Rectangle
|
||||||
@@ -336,10 +337,11 @@
|
|||||||
ItemContainerTransitions="{x:Null}"
|
ItemContainerTransitions="{x:Null}"
|
||||||
ItemTemplateSelector="{StaticResource PasteFormatTemplateSelector}"
|
ItemTemplateSelector="{StaticResource PasteFormatTemplateSelector}"
|
||||||
ItemsSource="{x:Bind ViewModel.CustomActionPasteFormats, Mode=OneWay}"
|
ItemsSource="{x:Bind ViewModel.CustomActionPasteFormats, Mode=OneWay}"
|
||||||
ScrollViewer.VerticalScrollBarVisibility="Visible"
|
ScrollViewer.VerticalScrollBarVisibility="Disabled"
|
||||||
ScrollViewer.VerticalScrollMode="Auto"
|
ScrollViewer.VerticalScrollMode="Disabled"
|
||||||
SelectionMode="None"
|
SelectionMode="None"
|
||||||
TabIndex="2" />
|
TabIndex="2" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
</ScrollViewer>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -215,7 +215,6 @@ public sealed class AdvancedAIKernelService : KernelServiceBase
|
|||||||
return new OpenAIPromptExecutionSettings
|
return new OpenAIPromptExecutionSettings
|
||||||
{
|
{
|
||||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
|
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
|
||||||
Temperature = 0.01,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using AdvancedPaste.Helpers;
|
||||||
using AdvancedPaste.Models;
|
using AdvancedPaste.Models;
|
||||||
using LanguageModelProvider;
|
using LanguageModelProvider;
|
||||||
using Microsoft.Extensions.AI;
|
using Microsoft.Extensions.AI;
|
||||||
@@ -33,10 +34,6 @@ public sealed class FoundryLocalPasteProvider : IPasteAIProvider
|
|||||||
_config = config;
|
_config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ProviderName => AIServiceType.FoundryLocal.ToNormalizedKey();
|
|
||||||
|
|
||||||
public string DisplayName => string.IsNullOrWhiteSpace(_config?.Model) ? "Foundry Local" : _config.Model;
|
|
||||||
|
|
||||||
public async Task<bool> IsAvailableAsync(CancellationToken cancellationToken)
|
public async Task<bool> IsAvailableAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
@@ -76,13 +73,20 @@ public sealed class FoundryLocalPasteProvider : IPasteAIProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
var chatClient = _modelProvider.GetIChatClient(modelReference);
|
|
||||||
if (chatClient is null)
|
IChatClient chatClient;
|
||||||
|
try
|
||||||
{
|
{
|
||||||
|
chatClient = _modelProvider.GetIChatClient(modelReference);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
// GetIChatClient throws InvalidOperationException for user-facing errors
|
||||||
|
var errorMessage = string.Format(System.Globalization.CultureInfo.CurrentCulture, ResourceLoaderInstance.ResourceLoader.GetString("FoundryLocal_UnableToLoadModel"), modelReference);
|
||||||
throw new PasteActionException(
|
throw new PasteActionException(
|
||||||
$"Unable to load Foundry Local model: {modelReference}",
|
errorMessage,
|
||||||
new InvalidOperationException("Chat client resolution failed"),
|
ex,
|
||||||
aiServiceMessage: "The model may not be downloaded or the Foundry Local service may not be running. Please check the model status in settings.");
|
aiServiceMessage: ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
var userMessageContent = $"""
|
var userMessageContent = $"""
|
||||||
@@ -142,6 +146,7 @@ public sealed class FoundryLocalPasteProvider : IPasteAIProvider
|
|||||||
var options = new ChatOptions
|
var options = new ChatOptions
|
||||||
{
|
{
|
||||||
ModelId = modelReference,
|
ModelId = modelReference,
|
||||||
|
MaxOutputTokens = 2048,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(systemPrompt))
|
if (!string.IsNullOrWhiteSpace(systemPrompt))
|
||||||
|
|||||||
@@ -157,8 +157,6 @@ namespace AdvancedPaste.Services.CustomActions
|
|||||||
{
|
{
|
||||||
AIServiceType.OpenAI or AIServiceType.AzureOpenAI => new OpenAIPromptExecutionSettings
|
AIServiceType.OpenAI or AIServiceType.AzureOpenAI => new OpenAIPromptExecutionSettings
|
||||||
{
|
{
|
||||||
Temperature = 0.01,
|
|
||||||
MaxTokens = 2000,
|
|
||||||
FunctionChoiceBehavior = null,
|
FunctionChoiceBehavior = null,
|
||||||
},
|
},
|
||||||
_ => new PromptExecutionSettings(),
|
_ => new PromptExecutionSettings(),
|
||||||
|
|||||||
@@ -160,10 +160,10 @@
|
|||||||
<value>Active provider: {0}</value>
|
<value>Active provider: {0}</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AIProvidersFlyoutHeader.Text" xml:space="preserve">
|
<data name="AIProvidersFlyoutHeader.Text" xml:space="preserve">
|
||||||
<value>AI providers</value>
|
<value>Configured models</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AIProvidersEmptyText.Text" xml:space="preserve">
|
<data name="AIProvidersEmptyText.Text" xml:space="preserve">
|
||||||
<value>No AI providers configured</value>
|
<value>No models configured</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AIProvidersManageButtonContent.Content" xml:space="preserve">
|
<data name="AIProvidersManageButtonContent.Content" xml:space="preserve">
|
||||||
<value>Configure models in Settings</value>
|
<value>Configure models in Settings</value>
|
||||||
@@ -364,8 +364,12 @@
|
|||||||
<data name="CustomEndpointWarning" xml:space="preserve">
|
<data name="CustomEndpointWarning" xml:space="preserve">
|
||||||
<value>You are using a custom endpoint. Verify all answers.</value>
|
<value>You are using a custom endpoint. Verify all answers.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="LocalModelBadge" xml:space="preserve">
|
<data name="LocalModelBadge.Text" xml:space="preserve">
|
||||||
<value>Local</value>
|
<value>Local</value>
|
||||||
<comment>Badge label displayed next to local AI model providers (e.g., Ollama, Foundry Local) to indicate the model runs locally</comment>
|
<comment>Badge label displayed next to local AI model providers (e.g., Ollama, Foundry Local) to indicate the model runs locally</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="FoundryLocal_UnableToLoadModel" xml:space="preserve">
|
||||||
|
<value>Unable to load Foundry Local model: {0}</value>
|
||||||
|
<comment>{0} is the model identifier. Do not translate {0}.</comment>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -271,7 +271,6 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
|||||||
|
|
||||||
if (wait == WAIT_OBJECT_0 + (hParent ? (hManualOverride ? 3 : 2) : 2))
|
if (wait == WAIT_OBJECT_0 + (hParent ? (hManualOverride ? 3 : 2) : 2))
|
||||||
{
|
{
|
||||||
Logger::info(L"[LightSwitchService] Settings file changed event detected.");
|
|
||||||
ResetEvent(hSettingsChanged);
|
ResetEvent(hSettingsChanged);
|
||||||
LightSwitchSettings::instance().LoadSettings();
|
LightSwitchSettings::instance().LoadSettings();
|
||||||
stateManager.OnSettingsChanged();
|
stateManager.OnSettingsChanged();
|
||||||
|
|||||||
@@ -17,12 +17,10 @@ LightSwitchStateManager::LightSwitchStateManager()
|
|||||||
void LightSwitchStateManager::OnSettingsChanged()
|
void LightSwitchStateManager::OnSettingsChanged()
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(_stateMutex);
|
std::lock_guard<std::mutex> lock(_stateMutex);
|
||||||
Logger::info(L"[LightSwitchStateManager] Settings changed event received");
|
|
||||||
|
|
||||||
// If manual override was active, clear it so new settings take effect
|
// If manual override was active, clear it so new settings take effect
|
||||||
if (_state.isManualOverride)
|
if (_state.isManualOverride)
|
||||||
{
|
{
|
||||||
Logger::info(L"[LightSwitchStateManager] Clearing manual override due to settings update.");
|
|
||||||
_state.isManualOverride = false;
|
_state.isManualOverride = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +31,6 @@ void LightSwitchStateManager::OnSettingsChanged()
|
|||||||
void LightSwitchStateManager::OnTick(int currentMinutes)
|
void LightSwitchStateManager::OnTick(int currentMinutes)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(_stateMutex);
|
std::lock_guard<std::mutex> lock(_stateMutex);
|
||||||
Logger::debug(L"[LightSwitchStateManager] Tick received: {}", currentMinutes);
|
|
||||||
EvaluateAndApplyIfNeeded();
|
EvaluateAndApplyIfNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +48,7 @@ void LightSwitchStateManager::OnManualOverride()
|
|||||||
|
|
||||||
_state.isAppsLightActive = GetCurrentAppsTheme();
|
_state.isAppsLightActive = GetCurrentAppsTheme();
|
||||||
|
|
||||||
Logger::info(L"[LightSwitchStateManager] Synced internal theme state to current system theme ({}) and apps theme ({}).",
|
Logger::debug(L"[LightSwitchStateManager] Synced internal theme state to current system theme ({}) and apps theme ({}).",
|
||||||
(_state.isSystemLightActive ? L"light" : L"dark"),
|
(_state.isSystemLightActive ? L"light" : L"dark"),
|
||||||
(_state.isAppsLightActive ? L"light" : L"dark"));
|
(_state.isAppsLightActive ? L"light" : L"dark"));
|
||||||
}
|
}
|
||||||
@@ -79,9 +76,9 @@ void LightSwitchStateManager::SyncInitialThemeState()
|
|||||||
std::lock_guard<std::mutex> lock(_stateMutex);
|
std::lock_guard<std::mutex> lock(_stateMutex);
|
||||||
_state.isSystemLightActive = GetCurrentSystemTheme();
|
_state.isSystemLightActive = GetCurrentSystemTheme();
|
||||||
_state.isAppsLightActive = GetCurrentAppsTheme();
|
_state.isAppsLightActive = GetCurrentAppsTheme();
|
||||||
Logger::info(L"[LightSwitchStateManager] Synced initial state to current system theme ({})",
|
Logger::debug(L"[LightSwitchStateManager] Synced initial state to current system theme ({})",
|
||||||
_state.isSystemLightActive ? L"light" : L"dark");
|
_state.isSystemLightActive ? L"light" : L"dark");
|
||||||
Logger::info(L"[LightSwitchStateManager] Synced initial state to current apps theme ({})",
|
Logger::debug(L"[LightSwitchStateManager] Synced initial state to current apps theme ({})",
|
||||||
_state.isAppsLightActive ? L"light" : L"dark");
|
_state.isAppsLightActive ? L"light" : L"dark");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +124,6 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
|
|||||||
// Early exit: OFF mode just pauses activity
|
// Early exit: OFF mode just pauses activity
|
||||||
if (_currentSettings.scheduleMode == ScheduleMode::Off)
|
if (_currentSettings.scheduleMode == ScheduleMode::Off)
|
||||||
{
|
{
|
||||||
Logger::debug(L"[LightSwitchStateManager] Mode is OFF — pausing service logic.");
|
|
||||||
_state.lastTickMinutes = now;
|
_state.lastTickMinutes = now;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -145,7 +141,6 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
|
|||||||
|
|
||||||
if (newDay || modeChangedToSun)
|
if (newDay || modeChangedToSun)
|
||||||
{
|
{
|
||||||
Logger::info(L"[LightSwitchStateManager] Recalculating sun times (mode/day change).");
|
|
||||||
auto [newLightTime, newDarkTime] = update_sun_times(_currentSettings);
|
auto [newLightTime, newDarkTime] = update_sun_times(_currentSettings);
|
||||||
_state.lastEvaluatedDay = st.wDay;
|
_state.lastEvaluatedDay = st.wDay;
|
||||||
_state.effectiveLightMinutes = newLightTime + _currentSettings.sunrise_offset;
|
_state.effectiveLightMinutes = newLightTime + _currentSettings.sunrise_offset;
|
||||||
@@ -188,12 +183,10 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
|
|||||||
|
|
||||||
if (crossedBoundary)
|
if (crossedBoundary)
|
||||||
{
|
{
|
||||||
Logger::info(L"[LightSwitchStateManager] Manual override cleared after crossing boundary.");
|
|
||||||
_state.isManualOverride = false;
|
_state.isManualOverride = false;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger::debug(L"[LightSwitchStateManager] Manual override active — skipping auto apply.");
|
|
||||||
_state.lastTickMinutes = now;
|
_state.lastTickMinutes = now;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -206,7 +199,7 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
|
|||||||
bool appsNeedsToChange = _currentSettings.changeApps && (_state.isAppsLightActive != shouldBeLight);
|
bool appsNeedsToChange = _currentSettings.changeApps && (_state.isAppsLightActive != shouldBeLight);
|
||||||
bool systemNeedsToChange = _currentSettings.changeSystem && (_state.isSystemLightActive != shouldBeLight);
|
bool systemNeedsToChange = _currentSettings.changeSystem && (_state.isSystemLightActive != shouldBeLight);
|
||||||
|
|
||||||
Logger::debug(
|
/* Logger::debug(
|
||||||
L"[LightSwitchStateManager] now = {:02d}:{:02d}, light boundary = {:02d}:{:02d} ({}), dark boundary = {:02d}:{:02d} ({})",
|
L"[LightSwitchStateManager] now = {:02d}:{:02d}, light boundary = {:02d}:{:02d} ({}), dark boundary = {:02d}:{:02d} ({})",
|
||||||
now / 60,
|
now / 60,
|
||||||
now % 60,
|
now % 60,
|
||||||
@@ -215,12 +208,12 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
|
|||||||
_state.effectiveLightMinutes,
|
_state.effectiveLightMinutes,
|
||||||
_state.effectiveDarkMinutes / 60,
|
_state.effectiveDarkMinutes / 60,
|
||||||
_state.effectiveDarkMinutes % 60,
|
_state.effectiveDarkMinutes % 60,
|
||||||
_state.effectiveDarkMinutes);
|
_state.effectiveDarkMinutes); */
|
||||||
|
|
||||||
Logger::debug("should be light = {}, apps needs change = {}, system needs change = {}",
|
/* Logger::debug("should be light = {}, apps needs change = {}, system needs change = {}",
|
||||||
shouldBeLight ? "true" : "false",
|
shouldBeLight ? "true" : "false",
|
||||||
appsNeedsToChange ? "true" : "false",
|
appsNeedsToChange ? "true" : "false",
|
||||||
systemNeedsToChange ? "true" : "false");
|
systemNeedsToChange ? "true" : "false"); */
|
||||||
|
|
||||||
// Only apply theme if there's a change or no override active
|
// Only apply theme if there's a change or no override active
|
||||||
if (!_state.isManualOverride && (appsNeedsToChange || systemNeedsToChange))
|
if (!_state.isManualOverride && (appsNeedsToChange || systemNeedsToChange))
|
||||||
@@ -230,10 +223,6 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
|
|||||||
|
|
||||||
_state.isSystemLightActive = GetCurrentSystemTheme();
|
_state.isSystemLightActive = GetCurrentSystemTheme();
|
||||||
_state.isAppsLightActive = GetCurrentAppsTheme();
|
_state.isAppsLightActive = GetCurrentAppsTheme();
|
||||||
|
|
||||||
Logger::debug(L"[LightSwitchStateManager] Synced post-apply theme state — System: {}, Apps: {}",
|
|
||||||
_state.isSystemLightActive ? L"light" : L"dark",
|
|
||||||
_state.isAppsLightActive ? L"light" : L"dark");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_state.lastTickMinutes = now;
|
_state.lastTickMinutes = now;
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ type_pEnableThemeDialogTexture pEnableThemeDialogTexture;
|
|||||||
#define WIN7_VERSION 0x106
|
#define WIN7_VERSION 0x106
|
||||||
#define WIN10_VERSION 0x206
|
#define WIN10_VERSION 0x206
|
||||||
|
|
||||||
|
// Default recording format frame rates
|
||||||
|
#define RECORDING_FORMAT_GIF_DEFAULT_FRAMERATE 15
|
||||||
|
#define RECORDING_FORMAT_MP4_DEFAULT_FRAMERATE 30
|
||||||
|
|
||||||
// Time that we'll cache live zoom window to avoid flicker
|
// Time that we'll cache live zoom window to avoid flicker
|
||||||
// of live zooming on Vista/ws2k8
|
// of live zooming on Vista/ws2k8
|
||||||
#define LIVEZOOM_WINDOW_TIMEOUT 2*3600*1000
|
#define LIVEZOOM_WINDOW_TIMEOUT 2*3600*1000
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ FONT 8, "MS Shell Dlg", 0, 0, 0x0
|
|||||||
BEGIN
|
BEGIN
|
||||||
DEFPUSHBUTTON "OK",IDOK,166,306,50,14
|
DEFPUSHBUTTON "OK",IDOK,166,306,50,14
|
||||||
PUSHBUTTON "Cancel",IDCANCEL,223,306,50,14
|
PUSHBUTTON "Cancel",IDCANCEL,223,306,50,14
|
||||||
LTEXT "ZoomIt v9.20",IDC_VERSION,42,7,73,10
|
LTEXT "ZoomIt v9.21",IDC_VERSION,42,7,73,10
|
||||||
LTEXT "Copyright © 2006-2025 Mark Russinovich",IDC_COPYRIGHT,42,17,231,8
|
LTEXT "Copyright © 2006-2025 Mark Russinovich",IDC_COPYRIGHT,42,17,231,8
|
||||||
CONTROL "<a HREF=""https://www.sysinternals.com"">Sysinternals - www.sysinternals.com</a>",IDC_LINK,
|
CONTROL "<a HREF=""https://www.sysinternals.com"">Sysinternals - www.sysinternals.com</a>",IDC_LINK,
|
||||||
"SysLink",WS_TABSTOP,42,26,150,9
|
"SysLink",WS_TABSTOP,42,26,150,9
|
||||||
|
|||||||
@@ -44,11 +44,11 @@ LOGFONT g_LogFont;
|
|||||||
BOOLEAN g_DemoTypeUserDriven = false;
|
BOOLEAN g_DemoTypeUserDriven = false;
|
||||||
TCHAR g_DemoTypeFile[MAX_PATH] = {0};
|
TCHAR g_DemoTypeFile[MAX_PATH] = {0};
|
||||||
DWORD g_DemoTypeSpeedSlider = static_cast<int>(((MIN_TYPING_SPEED - MAX_TYPING_SPEED) / 2) + MAX_TYPING_SPEED);
|
DWORD g_DemoTypeSpeedSlider = static_cast<int>(((MIN_TYPING_SPEED - MAX_TYPING_SPEED) / 2) + MAX_TYPING_SPEED);
|
||||||
DWORD g_RecordFrameRate = 30;
|
DWORD g_RecordFrameRate = 30; // We default to 30 here, but g_RecordFrameRate can be different depending on recording format and gets set accordingly
|
||||||
DWORD g_RecordScaling = 100;
|
DWORD g_RecordScaling = 100;
|
||||||
DWORD g_RecordScalingGIF = 50;
|
DWORD g_RecordScalingGIF = 50;
|
||||||
DWORD g_RecordScalingMP4 = 100;
|
DWORD g_RecordScalingMP4 = 100;
|
||||||
RecordingFormat g_RecordingFormat = RecordingFormat::GIF;
|
RecordingFormat g_RecordingFormat = RecordingFormat::MP4;
|
||||||
BOOLEAN g_CaptureAudio = FALSE;
|
BOOLEAN g_CaptureAudio = FALSE;
|
||||||
TCHAR g_MicrophoneDeviceId[MAX_PATH] = {0};
|
TCHAR g_MicrophoneDeviceId[MAX_PATH] = {0};
|
||||||
|
|
||||||
@@ -87,8 +87,7 @@ REG_SETTING RegSettings[] = {
|
|||||||
{ L"SnapToGrid", SETTING_TYPE_BOOLEAN, 0, &g_SnapToGrid, static_cast<DOUBLE>(g_SnapToGrid) },
|
{ L"SnapToGrid", SETTING_TYPE_BOOLEAN, 0, &g_SnapToGrid, static_cast<DOUBLE>(g_SnapToGrid) },
|
||||||
{ L"ZoominSliderLevel", SETTING_TYPE_DWORD, 0, &g_SliderZoomLevel, static_cast<DOUBLE>(g_SliderZoomLevel) },
|
{ L"ZoominSliderLevel", SETTING_TYPE_DWORD, 0, &g_SliderZoomLevel, static_cast<DOUBLE>(g_SliderZoomLevel) },
|
||||||
{ L"Font", SETTING_TYPE_BINARY, sizeof g_LogFont, &g_LogFont, static_cast<DOUBLE>(0) },
|
{ L"Font", SETTING_TYPE_BINARY, sizeof g_LogFont, &g_LogFont, static_cast<DOUBLE>(0) },
|
||||||
{ L"RecordFrameRate", SETTING_TYPE_DWORD, 0, &g_RecordFrameRate, static_cast<DOUBLE>(g_RecordFrameRate) },
|
{ L"RecordingFormat", SETTING_TYPE_DWORD, 0, &g_RecordingFormat, static_cast<DOUBLE>(g_RecordingFormat) },
|
||||||
{ L"RecordingFormat", SETTING_TYPE_DWORD, 0, &g_RecordingFormat, static_cast<DOUBLE>(0) },
|
|
||||||
{ L"RecordScalingGIF", SETTING_TYPE_DWORD, 0, &g_RecordScalingGIF, static_cast<DOUBLE>(g_RecordScalingGIF) },
|
{ L"RecordScalingGIF", SETTING_TYPE_DWORD, 0, &g_RecordScalingGIF, static_cast<DOUBLE>(g_RecordScalingGIF) },
|
||||||
{ L"RecordScalingMP4", SETTING_TYPE_DWORD, 0, &g_RecordScalingMP4, static_cast<DOUBLE>(g_RecordScalingMP4) },
|
{ L"RecordScalingMP4", SETTING_TYPE_DWORD, 0, &g_RecordScalingMP4, static_cast<DOUBLE>(g_RecordScalingMP4) },
|
||||||
{ L"CaptureAudio", SETTING_TYPE_BOOLEAN, 0, &g_CaptureAudio, static_cast<DOUBLE>(g_CaptureAudio) },
|
{ L"CaptureAudio", SETTING_TYPE_BOOLEAN, 0, &g_CaptureAudio, static_cast<DOUBLE>(g_CaptureAudio) },
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ BOOL g_RecordToggle = FALSE;
|
|||||||
BOOL g_RecordCropping = FALSE;
|
BOOL g_RecordCropping = FALSE;
|
||||||
SelectRectangle g_SelectRectangle;
|
SelectRectangle g_SelectRectangle;
|
||||||
std::wstring g_RecordingSaveLocation;
|
std::wstring g_RecordingSaveLocation;
|
||||||
|
std::wstring g_RecordingSaveLocationGIF;
|
||||||
winrt::IDirect3DDevice g_RecordDevice{ nullptr };
|
winrt::IDirect3DDevice g_RecordDevice{ nullptr };
|
||||||
std::shared_ptr<VideoRecordingSession> g_RecordingSession = nullptr;
|
std::shared_ptr<VideoRecordingSession> g_RecordingSession = nullptr;
|
||||||
std::shared_ptr<GifRecordingSession> g_GifRecordingSession = nullptr;
|
std::shared_ptr<GifRecordingSession> g_GifRecordingSession = nullptr;
|
||||||
@@ -2173,7 +2174,10 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
|
|||||||
CheckDlgButton( g_OptionsTabs[RECORD_PAGE].hPage, IDC_CAPTURE_AUDIO,
|
CheckDlgButton( g_OptionsTabs[RECORD_PAGE].hPage, IDC_CAPTURE_AUDIO,
|
||||||
g_CaptureAudio ? BST_CHECKED: BST_UNCHECKED );
|
g_CaptureAudio ? BST_CHECKED: BST_UNCHECKED );
|
||||||
|
|
||||||
for (int i = 0; i < _countof(g_FramerateOptions); i++) {
|
//
|
||||||
|
// The framerate drop down list is not used in the current version (might be added in the future)
|
||||||
|
//
|
||||||
|
/*for (int i = 0; i < _countof(g_FramerateOptions); i++) {
|
||||||
|
|
||||||
_stprintf(text, L"%d", g_FramerateOptions[i]);
|
_stprintf(text, L"%d", g_FramerateOptions[i]);
|
||||||
SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FRAME_RATE), static_cast<UINT>(CB_ADDSTRING),
|
SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FRAME_RATE), static_cast<UINT>(CB_ADDSTRING),
|
||||||
@@ -2182,7 +2186,7 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
|
|||||||
|
|
||||||
SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FRAME_RATE), CB_SETCURSEL, static_cast<WPARAM>(i), static_cast<LPARAM>(0));
|
SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FRAME_RATE), CB_SETCURSEL, static_cast<WPARAM>(i), static_cast<LPARAM>(0));
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
// Add the recording format to the combo box and set the current selection
|
// Add the recording format to the combo box and set the current selection
|
||||||
size_t selection = 0;
|
size_t selection = 0;
|
||||||
@@ -2345,17 +2349,8 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
|
|||||||
text[2] = 0;
|
text[2] = 0;
|
||||||
newTimeout = _tstoi( text );
|
newTimeout = _tstoi( text );
|
||||||
|
|
||||||
if( g_RecordingFormat == RecordingFormat::GIF )
|
|
||||||
{
|
|
||||||
// Hardcode lower frame rate for GIFs
|
|
||||||
g_RecordFrameRate = 15;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
g_RecordFrameRate = g_FramerateOptions[SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FRAME_RATE), static_cast<UINT>(CB_GETCURSEL), static_cast<WPARAM>(0), static_cast<LPARAM>(0))];
|
|
||||||
}
|
|
||||||
|
|
||||||
g_RecordingFormat = static_cast<RecordingFormat>(SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FORMAT), static_cast<UINT>(CB_GETCURSEL), static_cast<WPARAM>(0), static_cast<LPARAM>(0)));
|
g_RecordingFormat = static_cast<RecordingFormat>(SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FORMAT), static_cast<UINT>(CB_GETCURSEL), static_cast<WPARAM>(0), static_cast<LPARAM>(0)));
|
||||||
|
g_RecordFrameRate = (g_RecordingFormat == RecordingFormat::GIF) ? RECORDING_FORMAT_GIF_DEFAULT_FRAMERATE : RECORDING_FORMAT_MP4_DEFAULT_FRAMERATE;
|
||||||
g_RecordScaling = static_cast<int>(SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_SCALING), static_cast<UINT>(CB_GETCURSEL), static_cast<WPARAM>(0), static_cast<LPARAM>(0)) * 10 + 10);
|
g_RecordScaling = static_cast<int>(SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_SCALING), static_cast<UINT>(CB_GETCURSEL), static_cast<WPARAM>(0), static_cast<LPARAM>(0)) * 10 + 10);
|
||||||
|
|
||||||
// Get the selected microphone
|
// Get the selected microphone
|
||||||
@@ -3536,7 +3531,16 @@ void StopRecording()
|
|||||||
//----------------------------------------------------------------------------
|
//----------------------------------------------------------------------------
|
||||||
auto GetUniqueRecordingFilename()
|
auto GetUniqueRecordingFilename()
|
||||||
{
|
{
|
||||||
std::filesystem::path path{ g_RecordingSaveLocation };
|
std::filesystem::path path;
|
||||||
|
|
||||||
|
if (g_RecordingFormat == RecordingFormat::GIF)
|
||||||
|
{
|
||||||
|
path = g_RecordingSaveLocationGIF;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
path = g_RecordingSaveLocation;
|
||||||
|
}
|
||||||
|
|
||||||
// Chop off index if it's there
|
// Chop off index if it's there
|
||||||
auto base = std::regex_replace( path.stem().wstring(), std::wregex( L" [(][0-9]+[)]$" ), L"" );
|
auto base = std::regex_replace( path.stem().wstring(), std::wregex( L" [(][0-9]+[)]$" ), L"" );
|
||||||
@@ -3591,6 +3595,7 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
|
|||||||
auto stream = co_await file.OpenAsync( winrt::FileAccessMode::ReadWrite );
|
auto stream = co_await file.OpenAsync( winrt::FileAccessMode::ReadWrite );
|
||||||
|
|
||||||
// Create the appropriate recording session based on format
|
// Create the appropriate recording session based on format
|
||||||
|
OutputDebugStringW((L"Starting recording session. Framerate: " + std::to_wstring(g_RecordFrameRate) + L" scaling: " + std::to_wstring(g_RecordScaling) + L" Format: " + (g_RecordingFormat == RecordingFormat::GIF ? L"GIF" : L"MP4") + L"\n").c_str());
|
||||||
if (g_RecordingFormat == RecordingFormat::GIF)
|
if (g_RecordingFormat == RecordingFormat::GIF)
|
||||||
{
|
{
|
||||||
g_GifRecordingSession = GifRecordingSession::Create(
|
g_GifRecordingSession = GifRecordingSession::Create(
|
||||||
@@ -3657,18 +3662,44 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
|
|||||||
saveDialog->SetFileTypes( _countof( fileTypes ), fileTypes );
|
saveDialog->SetFileTypes( _countof( fileTypes ), fileTypes );
|
||||||
}
|
}
|
||||||
|
|
||||||
if( g_RecordingSaveLocation.size() == 0) {
|
// Peek the folder Windows has chosen to display
|
||||||
|
static std::filesystem::path lastSaveFolder;
|
||||||
|
wil::unique_cotaskmem_string chosenFolderPath;
|
||||||
|
wil::com_ptr<IShellItem> currentSelectedFolder;
|
||||||
|
bool bFolderChanged = false;
|
||||||
|
if (SUCCEEDED(saveDialog->GetFolder(currentSelectedFolder.put())))
|
||||||
|
{
|
||||||
|
if (SUCCEEDED(currentSelectedFolder->GetDisplayName(SIGDN_FILESYSPATH, chosenFolderPath.put())))
|
||||||
|
{
|
||||||
|
if (lastSaveFolder != chosenFolderPath.get())
|
||||||
|
{
|
||||||
|
lastSaveFolder = chosenFolderPath.get() ? chosenFolderPath.get() : std::filesystem::path{};
|
||||||
|
bFolderChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if( (g_RecordingFormat == RecordingFormat::GIF && g_RecordingSaveLocationGIF.size() == 0) || (g_RecordingFormat == RecordingFormat::MP4 && g_RecordingSaveLocation.size() == 0) || (bFolderChanged)) {
|
||||||
|
|
||||||
wil::com_ptr<IShellItem> shellItem;
|
wil::com_ptr<IShellItem> shellItem;
|
||||||
wil::unique_cotaskmem_string folderPath;
|
wil::unique_cotaskmem_string folderPath;
|
||||||
if (SUCCEEDED(saveDialog->GetFolder(shellItem.put())) && SUCCEEDED(shellItem->GetDisplayName(SIGDN_FILESYSPATH, folderPath.put())))
|
if (SUCCEEDED(saveDialog->GetFolder(shellItem.put())) && SUCCEEDED(shellItem->GetDisplayName(SIGDN_FILESYSPATH, folderPath.put()))) {
|
||||||
|
if (g_RecordingFormat == RecordingFormat::GIF) {
|
||||||
|
g_RecordingSaveLocationGIF = folderPath.get();
|
||||||
|
std::filesystem::path currentPath{ g_RecordingSaveLocationGIF };
|
||||||
|
g_RecordingSaveLocationGIF = currentPath / DEFAULT_GIF_RECORDING_FILE;
|
||||||
|
}
|
||||||
|
else {
|
||||||
g_RecordingSaveLocation = folderPath.get();
|
g_RecordingSaveLocation = folderPath.get();
|
||||||
|
if (g_RecordingFormat == RecordingFormat::MP4) {
|
||||||
|
std::filesystem::path currentPath{ g_RecordingSaveLocation };
|
||||||
|
g_RecordingSaveLocation = currentPath / DEFAULT_RECORDING_FILE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always use appropriate default filename based on current format
|
// Always use appropriate default filename based on current format
|
||||||
std::filesystem::path currentPath{ g_RecordingSaveLocation };
|
|
||||||
const wchar_t* defaultFile = (g_RecordingFormat == RecordingFormat::GIF) ? DEFAULT_GIF_RECORDING_FILE : DEFAULT_RECORDING_FILE;
|
|
||||||
g_RecordingSaveLocation = currentPath.parent_path() / defaultFile;
|
|
||||||
auto suggestedName = GetUniqueRecordingFilename();
|
auto suggestedName = GetUniqueRecordingFilename();
|
||||||
saveDialog->SetFileName( suggestedName.c_str() );
|
saveDialog->SetFileName( suggestedName.c_str() );
|
||||||
|
|
||||||
@@ -3696,10 +3727,16 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
||||||
co_await file.MoveAndReplaceAsync( destFile );
|
co_await file.MoveAndReplaceAsync(destFile);
|
||||||
|
if (g_RecordingFormat == RecordingFormat::GIF) {
|
||||||
|
g_RecordingSaveLocationGIF = file.Path();
|
||||||
|
SaveToClipboard(g_RecordingSaveLocationGIF.c_str(), hWnd);
|
||||||
|
}
|
||||||
|
else {
|
||||||
g_RecordingSaveLocation = file.Path();
|
g_RecordingSaveLocation = file.Path();
|
||||||
SaveToClipboard(g_RecordingSaveLocation.c_str(), hWnd);
|
SaveToClipboard(g_RecordingSaveLocation.c_str(), hWnd);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
g_bSaveInProgress = false;
|
g_bSaveInProgress = false;
|
||||||
|
|
||||||
SendMessage( g_hWndMain, WM_USER_RESTORE_CURSOR, 0, 0 );
|
SendMessage( g_hWndMain, WM_USER_RESTORE_CURSOR, 0, 0 );
|
||||||
@@ -4039,8 +4076,10 @@ LRESULT APIENTRY MainWndProc(
|
|||||||
// Set g_RecordScaling based on the current recording format
|
// Set g_RecordScaling based on the current recording format
|
||||||
if (g_RecordingFormat == RecordingFormat::GIF) {
|
if (g_RecordingFormat == RecordingFormat::GIF) {
|
||||||
g_RecordScaling = g_RecordScalingGIF;
|
g_RecordScaling = g_RecordScalingGIF;
|
||||||
|
g_RecordFrameRate = RECORDING_FORMAT_GIF_DEFAULT_FRAMERATE;
|
||||||
} else {
|
} else {
|
||||||
g_RecordScaling = g_RecordScalingMP4;
|
g_RecordScaling = g_RecordScalingMP4;
|
||||||
|
g_RecordFrameRate = RECORDING_FORMAT_MP4_DEFAULT_FRAMERATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// to support migrating from
|
// to support migrating from
|
||||||
@@ -6333,6 +6372,17 @@ LRESULT APIENTRY MainWndProc(
|
|||||||
// Reload the settings. This message is called from PowerToys after a setting is changed by the user.
|
// Reload the settings. This message is called from PowerToys after a setting is changed by the user.
|
||||||
reg.ReadRegSettings(RegSettings);
|
reg.ReadRegSettings(RegSettings);
|
||||||
|
|
||||||
|
if (g_RecordingFormat == RecordingFormat::GIF)
|
||||||
|
{
|
||||||
|
g_RecordScaling = g_RecordScalingGIF;
|
||||||
|
g_RecordFrameRate = RECORDING_FORMAT_GIF_DEFAULT_FRAMERATE;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
g_RecordScaling = g_RecordScalingMP4;
|
||||||
|
g_RecordFrameRate = RECORDING_FORMAT_MP4_DEFAULT_FRAMERATE;
|
||||||
|
}
|
||||||
|
|
||||||
// Apply tray icon setting
|
// Apply tray icon setting
|
||||||
EnableDisableTrayIcon(hWnd, g_ShowTrayIcon);
|
EnableDisableTrayIcon(hWnd, g_ShowTrayIcon);
|
||||||
|
|
||||||
|
|||||||
@@ -350,7 +350,7 @@ namespace Awake.Core
|
|||||||
TrayHelper.TimedIcon,
|
TrayHelper.TimedIcon,
|
||||||
TrayIconAction.Update);
|
TrayIconAction.Update);
|
||||||
},
|
},
|
||||||
_ => HandleTimerCompletion("timed"),
|
() => HandleTimerCompletion("timed"),
|
||||||
_tokenSource.Token);
|
_tokenSource.Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
|||||||
|
|
||||||
public string Title { get => string.IsNullOrEmpty(field) ? Name : field; protected set; } = string.Empty;
|
public string Title { get => string.IsNullOrEmpty(field) ? Name : field; protected set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Id { get; protected set; } = string.Empty;
|
||||||
|
|
||||||
// This property maps to `IPage.IsLoading`, but we want to expose our own
|
// This property maps to `IPage.IsLoading`, but we want to expose our own
|
||||||
// `IsLoading` property as a combo of this value and `IsInitialized`
|
// `IsLoading` property as a combo of this value and `IsInitialized`
|
||||||
public bool ModelIsLoading { get; protected set; } = true;
|
public bool ModelIsLoading { get; protected set; } = true;
|
||||||
@@ -142,6 +144,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
|||||||
return; // throw?
|
return; // throw?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Id = page.Id;
|
||||||
Name = page.Name;
|
Name = page.Name;
|
||||||
ModelIsLoading = page.IsLoading;
|
ModelIsLoading = page.IsLoading;
|
||||||
Title = page.Title;
|
Title = page.Title;
|
||||||
|
|||||||
@@ -2,21 +2,52 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
using Windows.Graphics;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||||
|
|
||||||
public sealed class WindowPosition
|
public sealed class WindowPosition
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets left position in device pixels.
|
||||||
|
/// </summary>
|
||||||
public int X { get; set; }
|
public int X { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets top position in device pixels.
|
||||||
|
/// </summary>
|
||||||
public int Y { get; set; }
|
public int Y { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets width in device pixels.
|
||||||
|
/// </summary>
|
||||||
public int Width { get; set; }
|
public int Width { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets height in device pixels.
|
||||||
|
/// </summary>
|
||||||
public int Height { get; set; }
|
public int Height { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets width of the screen in device pixels where the window is located.
|
||||||
|
/// </summary>
|
||||||
|
public int ScreenWidth { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets height of the screen in device pixels where the window is located.
|
||||||
|
/// </summary>
|
||||||
|
public int ScreenHeight { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets DPI (dots per inch) of the display where the window is located.
|
||||||
|
/// </summary>
|
||||||
|
public int Dpi { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts the window position properties to a <see cref="RectInt32"/> structure representing the physical window rectangle.
|
||||||
|
/// </summary>
|
||||||
|
public RectInt32 ToPhysicalWindowRectangle()
|
||||||
|
{
|
||||||
|
return new RectInt32(X, Y, Width, Height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,12 @@ public class OpenPage : EventBase, IEvent
|
|||||||
{
|
{
|
||||||
public int PageDepth { get; set; }
|
public int PageDepth { get; set; }
|
||||||
|
|
||||||
public OpenPage(int pageDepth)
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
public OpenPage(int pageDepth, string id)
|
||||||
{
|
{
|
||||||
PageDepth = pageDepth;
|
PageDepth = pageDepth;
|
||||||
|
Id = id;
|
||||||
|
|
||||||
EventName = "CmdPal_OpenPage";
|
EventName = "CmdPal_OpenPage";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ using Microsoft.CmdPal.UI.ViewModels;
|
|||||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.PowerToys.Telemetry;
|
using Microsoft.PowerToys.Telemetry;
|
||||||
|
using Microsoft.UI;
|
||||||
using Microsoft.UI.Composition;
|
using Microsoft.UI.Composition;
|
||||||
using Microsoft.UI.Composition.SystemBackdrops;
|
using Microsoft.UI.Composition.SystemBackdrops;
|
||||||
using Microsoft.UI.Input;
|
using Microsoft.UI.Input;
|
||||||
@@ -33,6 +34,8 @@ using Windows.UI.WindowManagement;
|
|||||||
using Windows.Win32;
|
using Windows.Win32;
|
||||||
using Windows.Win32.Foundation;
|
using Windows.Win32.Foundation;
|
||||||
using Windows.Win32.Graphics.Dwm;
|
using Windows.Win32.Graphics.Dwm;
|
||||||
|
using Windows.Win32.Graphics.Gdi;
|
||||||
|
using Windows.Win32.UI.HiDpi;
|
||||||
using Windows.Win32.UI.Input.KeyboardAndMouse;
|
using Windows.Win32.UI.Input.KeyboardAndMouse;
|
||||||
using Windows.Win32.UI.WindowsAndMessaging;
|
using Windows.Win32.UI.WindowsAndMessaging;
|
||||||
using WinRT;
|
using WinRT;
|
||||||
@@ -48,6 +51,9 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
IRecipient<QuitMessage>,
|
IRecipient<QuitMessage>,
|
||||||
IDisposable
|
IDisposable
|
||||||
{
|
{
|
||||||
|
private const int DefaultWidth = 800;
|
||||||
|
private const int DefaultHeight = 480;
|
||||||
|
|
||||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Stylistically, window messages are WM_")]
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Stylistically, window messages are WM_")]
|
||||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Stylistically, window messages are WM_")]
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Stylistically, window messages are WM_")]
|
||||||
private readonly uint WM_TASKBAR_RESTART;
|
private readonly uint WM_TASKBAR_RESTART;
|
||||||
@@ -173,22 +179,8 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
AppWindow.Resize(new SizeInt32 { Width = savedPosition.Width, Height = savedPosition.Height });
|
var newRect = EnsureWindowIsVisible(savedPosition.ToPhysicalWindowRectangle(), new SizeInt32(savedPosition.ScreenWidth, savedPosition.ScreenHeight), savedPosition.Dpi);
|
||||||
|
AppWindow.MoveAndResize(newRect);
|
||||||
var savedRect = new RectInt32(savedPosition.X, savedPosition.Y, savedPosition.Width, savedPosition.Height);
|
|
||||||
var displayArea = DisplayArea.GetFromRect(savedRect, DisplayAreaFallback.Nearest);
|
|
||||||
var workArea = displayArea.WorkArea;
|
|
||||||
|
|
||||||
var maxX = workArea.X + Math.Max(0, workArea.Width - savedPosition.Width);
|
|
||||||
var maxY = workArea.Y + Math.Max(0, workArea.Height - savedPosition.Height);
|
|
||||||
|
|
||||||
var targetPoint = new PointInt32
|
|
||||||
{
|
|
||||||
X = Math.Clamp(savedPosition.X, workArea.X, maxX),
|
|
||||||
Y = Math.Clamp(savedPosition.Y, workArea.Y, maxY),
|
|
||||||
};
|
|
||||||
|
|
||||||
AppWindow.Move(targetPoint);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PositionCentered(DisplayArea displayArea)
|
private void PositionCentered(DisplayArea displayArea)
|
||||||
@@ -207,12 +199,16 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
|
|
||||||
private void UpdateWindowPositionInMemory()
|
private void UpdateWindowPositionInMemory()
|
||||||
{
|
{
|
||||||
|
var displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest) ?? DisplayArea.Primary;
|
||||||
_currentWindowPosition = new WindowPosition
|
_currentWindowPosition = new WindowPosition
|
||||||
{
|
{
|
||||||
X = AppWindow.Position.X,
|
X = AppWindow.Position.X,
|
||||||
Y = AppWindow.Position.Y,
|
Y = AppWindow.Position.Y,
|
||||||
Width = AppWindow.Size.Width,
|
Width = AppWindow.Size.Width,
|
||||||
Height = AppWindow.Size.Height,
|
Height = AppWindow.Size.Height,
|
||||||
|
Dpi = (int)this.GetDpiForWindow(),
|
||||||
|
ScreenWidth = displayArea.WorkArea.Width,
|
||||||
|
ScreenHeight = displayArea.WorkArea.Height,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,8 +296,8 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
|
|
||||||
if (target == MonitorBehavior.ToLast)
|
if (target == MonitorBehavior.ToLast)
|
||||||
{
|
{
|
||||||
AppWindow.Resize(new SizeInt32 { Width = _currentWindowPosition.Width, Height = _currentWindowPosition.Height });
|
var newRect = EnsureWindowIsVisible(_currentWindowPosition.ToPhysicalWindowRectangle(), new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight), _currentWindowPosition.Dpi);
|
||||||
AppWindow.Move(new PointInt32 { X = _currentWindowPosition.X, Y = _currentWindowPosition.Y });
|
AppWindow.MoveAndResize(newRect);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -330,6 +326,114 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
PInvoke.SetWindowPos(hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
|
PInvoke.SetWindowPos(hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures that the window rectangle is visible on-screen.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="windowRect">The window rectangle in physical pixels.</param>
|
||||||
|
/// <param name="originalScreen">The desktop area the window was positioned on.</param>
|
||||||
|
/// <param name="originalDpi">The window's original DPI.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A window rectangle in physical pixels, moved to the nearest display and resized
|
||||||
|
/// if the DPI has changed.
|
||||||
|
/// </returns>
|
||||||
|
private static RectInt32 EnsureWindowIsVisible(RectInt32 windowRect, SizeInt32 originalScreen, int originalDpi)
|
||||||
|
{
|
||||||
|
var displayArea = DisplayArea.GetFromRect(windowRect, DisplayAreaFallback.Nearest);
|
||||||
|
if (displayArea is null)
|
||||||
|
{
|
||||||
|
return windowRect;
|
||||||
|
}
|
||||||
|
|
||||||
|
var workArea = displayArea.WorkArea;
|
||||||
|
if (workArea.Width <= 0 || workArea.Height <= 0)
|
||||||
|
{
|
||||||
|
// Fallback, nothing reasonable to do
|
||||||
|
return windowRect;
|
||||||
|
}
|
||||||
|
|
||||||
|
var effectiveDpi = GetEffectiveDpiFromDisplayId(displayArea);
|
||||||
|
if (originalDpi <= 0)
|
||||||
|
{
|
||||||
|
originalDpi = effectiveDpi; // use current DPI as baseline (no scaling adjustment needed)
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasInvalidSize = windowRect.Width <= 0 || windowRect.Height <= 0;
|
||||||
|
if (hasInvalidSize)
|
||||||
|
{
|
||||||
|
windowRect = new RectInt32(windowRect.X, windowRect.Y, DefaultWidth, DefaultHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a DPI change, scale the window rectangle accordingly
|
||||||
|
if (effectiveDpi != originalDpi)
|
||||||
|
{
|
||||||
|
var scalingFactor = effectiveDpi / (double)originalDpi;
|
||||||
|
windowRect = new RectInt32(
|
||||||
|
(int)Math.Round(windowRect.X * scalingFactor),
|
||||||
|
(int)Math.Round(windowRect.Y * scalingFactor),
|
||||||
|
(int)Math.Round(windowRect.Width * scalingFactor),
|
||||||
|
(int)Math.Round(windowRect.Height * scalingFactor));
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetWidth = Math.Min(windowRect.Width, workArea.Width);
|
||||||
|
var targetHeight = Math.Min(windowRect.Height, workArea.Height);
|
||||||
|
|
||||||
|
// Ensure at least some minimum visible area (e.g., 100 pixels)
|
||||||
|
// This helps prevent the window from being entirely offscreen, regardless of display scaling.
|
||||||
|
const int minimumVisibleSize = 100;
|
||||||
|
var isOffscreen =
|
||||||
|
windowRect.X + minimumVisibleSize > workArea.X + workArea.Width ||
|
||||||
|
windowRect.X + windowRect.Width - minimumVisibleSize < workArea.X ||
|
||||||
|
windowRect.Y + minimumVisibleSize > workArea.Y + workArea.Height ||
|
||||||
|
windowRect.Y + windowRect.Height - minimumVisibleSize < workArea.Y;
|
||||||
|
|
||||||
|
// if the work area size has changed, re-center the window
|
||||||
|
var workAreaSizeChanged =
|
||||||
|
originalScreen.Width != workArea.Width ||
|
||||||
|
originalScreen.Height != workArea.Height;
|
||||||
|
|
||||||
|
int targetX;
|
||||||
|
int targetY;
|
||||||
|
var recenter = isOffscreen || workAreaSizeChanged || hasInvalidSize;
|
||||||
|
if (recenter)
|
||||||
|
{
|
||||||
|
targetX = workArea.X + ((workArea.Width - targetWidth) / 2);
|
||||||
|
targetY = workArea.Y + ((workArea.Height - targetHeight) / 2);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
targetX = windowRect.X;
|
||||||
|
targetY = windowRect.Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RectInt32(targetX, targetY, targetWidth, targetHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetEffectiveDpiFromDisplayId(DisplayArea displayArea)
|
||||||
|
{
|
||||||
|
var effectiveDpi = 96;
|
||||||
|
|
||||||
|
var hMonitor = (HMONITOR)Win32Interop.GetMonitorFromDisplayId(displayArea.DisplayId);
|
||||||
|
if (!hMonitor.IsNull)
|
||||||
|
{
|
||||||
|
var hr = PInvoke.GetDpiForMonitor(hMonitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var dpiX, out _);
|
||||||
|
if (hr == 0)
|
||||||
|
{
|
||||||
|
effectiveDpi = (int)dpiX;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"GetDpiForMonitor failed with HRESULT: 0x{hr.Value:X8} on display {displayArea.DisplayId}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effectiveDpi <= 0)
|
||||||
|
{
|
||||||
|
effectiveDpi = 96;
|
||||||
|
}
|
||||||
|
|
||||||
|
return effectiveDpi;
|
||||||
|
}
|
||||||
|
|
||||||
private DisplayArea GetScreen(HWND currentHwnd, MonitorBehavior target)
|
private DisplayArea GetScreen(HWND currentHwnd, MonitorBehavior target)
|
||||||
{
|
{
|
||||||
// Leaving a note here, in case we ever need it:
|
// Leaving a note here, in case we ever need it:
|
||||||
@@ -479,6 +583,9 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
Y = _currentWindowPosition.Y,
|
Y = _currentWindowPosition.Y,
|
||||||
Width = _currentWindowPosition.Width,
|
Width = _currentWindowPosition.Width,
|
||||||
Height = _currentWindowPosition.Height,
|
Height = _currentWindowPosition.Height,
|
||||||
|
Dpi = _currentWindowPosition.Dpi,
|
||||||
|
ScreenWidth = _currentWindowPosition.ScreenWidth,
|
||||||
|
ScreenHeight = _currentWindowPosition.ScreenHeight,
|
||||||
};
|
};
|
||||||
|
|
||||||
SettingsModel.SaveSettings(settings);
|
SettingsModel.SaveSettings(settings);
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ using Microsoft.PowerToys.Telemetry;
|
|||||||
using Microsoft.UI.Dispatching;
|
using Microsoft.UI.Dispatching;
|
||||||
using Microsoft.UI.Input;
|
using Microsoft.UI.Input;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Automation.Peers;
|
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Microsoft.UI.Xaml.Input;
|
using Microsoft.UI.Xaml.Input;
|
||||||
using Microsoft.UI.Xaml.Media.Animation;
|
using Microsoft.UI.Xaml.Media.Animation;
|
||||||
@@ -160,7 +159,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
|||||||
new AsyncNavigationRequest(message.Page, message.CancellationToken),
|
new AsyncNavigationRequest(message.Page, message.CancellationToken),
|
||||||
message.WithAnimation ? DefaultPageAnimation : _noAnimation);
|
message.WithAnimation ? DefaultPageAnimation : _noAnimation);
|
||||||
|
|
||||||
PowerToysTelemetry.Log.WriteEvent(new OpenPage(RootFrame.BackStackDepth));
|
PowerToysTelemetry.Log.WriteEvent(new OpenPage(RootFrame.BackStackDepth, message.Page.Id));
|
||||||
|
|
||||||
if (!ViewModel.IsNested)
|
if (!ViewModel.IsNested)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,11 @@
|
|||||||
"nativeDebugging": false,
|
"nativeDebugging": false,
|
||||||
"doNotLaunchApp": false
|
"doNotLaunchApp": false
|
||||||
},
|
},
|
||||||
|
"Microsoft.CmdPal.UI (Package) + Native debugging": {
|
||||||
|
"commandName": "MsixPackage",
|
||||||
|
"nativeDebugging": true,
|
||||||
|
"doNotLaunchApp": false
|
||||||
|
},
|
||||||
"Microsoft.CmdPal.UI (Unpackaged)": {
|
"Microsoft.CmdPal.UI (Unpackaged)": {
|
||||||
"commandName": "Project"
|
"commandName": "Project"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,13 +24,13 @@
|
|||||||
<ApplicationIcon>Resources\ImageResizer.ico</ApplicationIcon>
|
<ApplicationIcon>Resources\ImageResizer.ico</ApplicationIcon>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<!-- <PropertyGroup>
|
||||||
<ApplicationManifest>ImageResizerUI.dev.manifest</ApplicationManifest>
|
<ApplicationManifest>ImageResizerUI.dev.manifest</ApplicationManifest>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(CIBuild)'=='true'">
|
<PropertyGroup Condition="'$(CIBuild)'=='true'">
|
||||||
<ApplicationManifest>ImageResizerUI.prod.manifest</ApplicationManifest>
|
<ApplicationManifest>ImageResizerUI.prod.manifest</ApplicationManifest>
|
||||||
</PropertyGroup>
|
</PropertyGroup> -->
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Update="Properties\Resources.resx">
|
<EmbeddedResource Update="Properties\Resources.resx">
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ public static class AIServiceTypeRegistry
|
|||||||
{
|
{
|
||||||
ServiceType = AIServiceType.AzureAIInference,
|
ServiceType = AIServiceType.AzureAIInference,
|
||||||
DisplayName = "Azure AI Inference",
|
DisplayName = "Azure AI Inference",
|
||||||
IconPath = "ms-appx:///Assets/Settings/Icons/Models/FoundryLocal.svg", // No icon for Azure AI Inference, use Foundry Local temporarily
|
IconPath = "ms-appx:///Assets/Settings/Icons/Models/Azure.svg",
|
||||||
IsOnlineService = true,
|
IsOnlineService = true,
|
||||||
LegalDescription = "AdvancedPaste_AzureAIInference_LegalDescription",
|
LegalDescription = "AdvancedPaste_AzureAIInference_LegalDescription",
|
||||||
TermsLabel = "AdvancedPaste_AzureAIInference_TermsLabel",
|
TermsLabel = "AdvancedPaste_AzureAIInference_TermsLabel",
|
||||||
|
|||||||
@@ -1,59 +1,34 @@
|
|||||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g clip-path="url(#clip0_2092_1741)">
|
<path d="M10.2663 0H0.231885C0.103572 0 0 0.10358 0 0.231885V2.55072C0 2.67904 0.103572 2.78261 0.231885 2.78261H12.5217C12.9059 2.78261 13.2174 3.09411 13.2174 3.47826V3.18995C13.2174 1.53971 11.9861 0 10.2663 0Z" fill="url(#paint0_linear_178_3940)"/>
|
||||||
<mask id="mask0_2092_1741" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="17" height="16">
|
<path d="M12.2334 0.81543C12.8633 1.44693 13.2174 2.29872 13.2174 3.19069V15.7689C13.2174 15.8972 13.3209 16.0007 13.4492 16.0007H15.7681C15.8964 16.0007 16 15.8972 16 15.7689V5.73524C16 4.99707 15.707 4.28983 15.1853 3.76732L12.2334 0.81543Z" fill="url(#paint1_linear_178_3940)"/>
|
||||||
<path d="M16.5 0H0.5V16H16.5V0Z" fill="white"/>
|
<path d="M6.78804 3.47852H0.231885C0.103572 3.47852 0 3.58209 0 3.71039V6.02921C0 6.1575 0.103572 6.26112 0.231885 6.26112H9.04346C9.42759 6.26112 9.7391 6.57263 9.7391 6.95676V6.6685C9.7391 5.01822 8.50778 3.47852 6.78804 3.47852Z" fill="url(#paint2_linear_178_3940)"/>
|
||||||
</mask>
|
<path d="M8.75537 4.29297C9.38531 4.92446 9.73928 5.77628 9.73928 6.6682V15.7681C9.73928 15.8964 9.8429 16 9.97119 16H12.29C12.4183 16 12.5219 15.8964 12.5219 15.7681V9.21281C12.5219 8.47462 12.229 7.76735 11.7072 7.24482L8.75537 4.29297Z" fill="url(#paint3_linear_178_3940)"/>
|
||||||
<g mask="url(#mask0_2092_1741)">
|
<path d="M3.30975 6.95703H0.231885C0.103572 6.95703 0 7.06056 0 7.18886V9.50771C0 9.63609 0.103572 9.73962 0.231885 9.73962H5.56521C5.94936 9.73962 6.26087 10.0511 6.26087 10.4353V10.147C6.26087 8.49675 5.02956 6.95703 3.30975 6.95703Z" fill="url(#paint4_linear_178_3940)"/>
|
||||||
<mask id="mask1_2092_1741" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="-1" y="-2" width="19" height="20">
|
<path d="M5.27686 7.77148C5.90677 8.40302 6.26083 9.25477 6.26083 10.1468V15.7684C6.26083 15.8967 6.36436 16.0003 6.49274 16.0003H8.8115C8.93988 16.0003 9.04341 15.8967 9.04341 15.7684V12.6913C9.04341 11.9531 8.75051 11.2459 8.22874 10.7234L5.27686 7.77148Z" fill="url(#paint5_linear_178_3940)"/>
|
||||||
<path d="M17.8337 -1.33337H-0.833008V17.3333H17.8337V-1.33337Z" fill="white"/>
|
|
||||||
</mask>
|
|
||||||
<g mask="url(#mask1_2092_1741)">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.1137 0.315668C11.57 0.315668 11.9744 0.657891 12.1196 1.15567C12.2648 1.65345 13.1152 4.73345 13.1152 4.73345V10.852H10.0352L10.0974 0.305298H11.1137V0.315668Z" fill="url(#paint0_linear_2092_1741)"/>
|
|
||||||
<path d="M15.6352 5.09586C15.6352 4.87808 15.4589 4.71216 15.2515 4.71216H13.4366C12.1611 4.71216 11.124 5.7492 11.124 7.02472V10.8618H13.3226C14.5982 10.8618 15.6352 9.82472 15.6352 8.54919V5.09586Z" fill="url(#paint1_linear_2092_1741)"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.1133 0.315674C10.7607 0.315674 10.4807 0.595674 10.4807 0.948265L10.4185 12.5942C10.4185 14.2949 9.0392 15.6742 7.33847 15.6742H1.74885C1.47921 15.6742 1.30292 15.4149 1.38589 15.1661L5.86589 2.37938C6.30144 1.14531 7.46293 0.315674 8.7696 0.315674H11.1237H11.1133Z" fill="url(#paint2_linear_2092_1741)"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="paint0_linear_2092_1741" x1="12.3996" y1="11.0801" x2="9.80702" y2="0.699373" gradientUnits="userSpaceOnUse">
|
<linearGradient id="paint0_linear_178_3940" x1="13.2174" y1="3.15349" x2="0" y2="3.15349" gradientUnits="userSpaceOnUse">
|
||||||
<stop stop-color="#712575"/>
|
<stop stop-color="#2C08AC"/>
|
||||||
<stop offset="0.09" stop-color="#9A2884"/>
|
<stop offset="0.8" stop-color="#4F42FD"/>
|
||||||
<stop offset="0.18" stop-color="#BF2C92"/>
|
|
||||||
<stop offset="0.27" stop-color="#DA2E9C"/>
|
|
||||||
<stop offset="0.34" stop-color="#EB30A2"/>
|
|
||||||
<stop offset="0.4" stop-color="#F131A5"/>
|
|
||||||
<stop offset="0.5" stop-color="#EC30A3"/>
|
|
||||||
<stop offset="0.61" stop-color="#DF2F9E"/>
|
|
||||||
<stop offset="0.72" stop-color="#C92D96"/>
|
|
||||||
<stop offset="0.83" stop-color="#AA2A8A"/>
|
|
||||||
<stop offset="0.95" stop-color="#83267C"/>
|
|
||||||
<stop offset="1" stop-color="#712575"/>
|
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient id="paint1_linear_2092_1741" x1="13.3848" y1="0.532897" x2="13.3848" y2="15.1759" gradientUnits="userSpaceOnUse">
|
<linearGradient id="paint1_linear_178_3940" x1="14.2303" y1="0.81543" x2="23.44" y2="11.9747" gradientUnits="userSpaceOnUse">
|
||||||
<stop stop-color="#DA7ED0"/>
|
<stop offset="0.3" stop-color="#7274FF"/>
|
||||||
<stop offset="0.08" stop-color="#B17BD5"/>
|
<stop offset="1" stop-color="#4F42FD"/>
|
||||||
<stop offset="0.19" stop-color="#8778DB"/>
|
|
||||||
<stop offset="0.3" stop-color="#6276E1"/>
|
|
||||||
<stop offset="0.41" stop-color="#4574E5"/>
|
|
||||||
<stop offset="0.54" stop-color="#2E72E8"/>
|
|
||||||
<stop offset="0.67" stop-color="#1D71EB"/>
|
|
||||||
<stop offset="0.81" stop-color="#1471EC"/>
|
|
||||||
<stop offset="1" stop-color="#1171ED"/>
|
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient id="paint2_linear_2092_1741" x1="12.5029" y1="0.865306" x2="2.79625" y2="16.4313" gradientUnits="userSpaceOnUse">
|
<linearGradient id="paint2_linear_178_3940" x1="9.7391" y1="6.63202" x2="0" y2="6.63202" gradientUnits="userSpaceOnUse">
|
||||||
<stop stop-color="#DA7ED0"/>
|
<stop stop-color="#2C08AC"/>
|
||||||
<stop offset="0.05" stop-color="#B77BD4"/>
|
<stop offset="0.8" stop-color="#4F42FD"/>
|
||||||
<stop offset="0.11" stop-color="#9079DA"/>
|
</linearGradient>
|
||||||
<stop offset="0.18" stop-color="#6E77DF"/>
|
<linearGradient id="paint3_linear_178_3940" x1="10.7523" y1="4.29297" x2="17.3026" y2="14.5881" gradientUnits="userSpaceOnUse">
|
||||||
<stop offset="0.25" stop-color="#5175E3"/>
|
<stop offset="0.3" stop-color="#7274FF"/>
|
||||||
<stop offset="0.33" stop-color="#3973E7"/>
|
<stop offset="1" stop-color="#4F42FD"/>
|
||||||
<stop offset="0.42" stop-color="#2772E9"/>
|
</linearGradient>
|
||||||
<stop offset="0.54" stop-color="#1A71EB"/>
|
<linearGradient id="paint4_linear_178_3940" x1="6.26087" y1="9.91172" x2="0" y2="9.91172" gradientUnits="userSpaceOnUse">
|
||||||
<stop offset="0.68" stop-color="#1371EC"/>
|
<stop stop-color="#2C08AC"/>
|
||||||
<stop offset="1" stop-color="#1171ED"/>
|
<stop offset="0.8" stop-color="#4F42FD"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint5_linear_178_3940" x1="7.2738" y1="7.77148" x2="11.0624" y2="16.243" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.3" stop-color="#7274FF"/>
|
||||||
|
<stop offset="1" stop-color="#4F42FD"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<clipPath id="clip0_2092_1741">
|
|
||||||
<rect width="16" height="16" fill="white" transform="translate(0.5)"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 318 KiB |
@@ -28,6 +28,8 @@
|
|||||||
<None Remove="Assets\Settings\Icons\Models\WindowsML.svg" />
|
<None Remove="Assets\Settings\Icons\Models\WindowsML.svg" />
|
||||||
<None Remove="Assets\Settings\Modules\APDialog.dark.png" />
|
<None Remove="Assets\Settings\Modules\APDialog.dark.png" />
|
||||||
<None Remove="Assets\Settings\Modules\APDialog.light.png" />
|
<None Remove="Assets\Settings\Modules\APDialog.light.png" />
|
||||||
|
<None Remove="Assets\Settings\Modules\CmdPal_Background.png" />
|
||||||
|
<None Remove="Assets\Settings\Modules\CmdPal_Hero.png" />
|
||||||
<None Remove="Assets\Settings\Modules\LightSwitch.png" />
|
<None Remove="Assets\Settings\Modules\LightSwitch.png" />
|
||||||
<None Remove="SettingsXAML\Controls\Dashboard\CheckUpdateControl.xaml" />
|
<None Remove="SettingsXAML\Controls\Dashboard\CheckUpdateControl.xaml" />
|
||||||
<None Remove="SettingsXAML\Controls\Dashboard\ShortcutConflictControl.xaml" />
|
<None Remove="SettingsXAML\Controls\Dashboard\ShortcutConflictControl.xaml" />
|
||||||
@@ -71,6 +73,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CommunityToolkit.Labs.WinUI.Controls.OpacityMaskView" />
|
||||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" />
|
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" />
|
||||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" />
|
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" />
|
||||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" />
|
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" />
|
||||||
|
|||||||
@@ -30,6 +30,24 @@
|
|||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<InfoBar
|
||||||
|
x:Uid="AdvancedPaste_FL_PreviewMessage"
|
||||||
|
Grid.Row="1"
|
||||||
|
Padding="4"
|
||||||
|
IsClosable="False"
|
||||||
|
IsOpen="True"
|
||||||
|
Message="Foundry Local is still in Public Preview">
|
||||||
|
<InfoBar.ActionButton>
|
||||||
|
<HyperlinkButton
|
||||||
|
x:Uid="AdvancedPaste_FL_LearnMoreFoundryLocal"
|
||||||
|
Content="Learn more"
|
||||||
|
NavigateUri="https://learn.microsoft.com/azure/ai-foundry/foundry-local/what-is-foundry-local" />
|
||||||
|
</InfoBar.ActionButton>
|
||||||
|
</InfoBar>
|
||||||
<StackPanel
|
<StackPanel
|
||||||
x:Name="LoadingPanel"
|
x:Name="LoadingPanel"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
@@ -69,12 +87,12 @@
|
|||||||
FontSize="24"
|
FontSize="24"
|
||||||
Glyph="" />
|
Glyph="" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
x:Uid="AdvancedPaste_FL_NoModelsDownloaded."
|
x:Uid="AdvancedPaste_FL_NoModelsDownloaded"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||||
TextAlignment="Center" />
|
TextAlignment="Center" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
x:Uid="AdvancedPaste_FL_RunFoundryLocalText.Text"
|
x:Uid="AdvancedPaste_FL_RunFoundryLocalText"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
FontSize="12"
|
FontSize="12"
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
@@ -106,7 +124,7 @@
|
|||||||
<ComboBox.Header>
|
<ComboBox.Header>
|
||||||
<TextBlock>
|
<TextBlock>
|
||||||
<Run x:Uid="AdvancedPaste_FL_LocalModel" /><LineBreak /><Run
|
<Run x:Uid="AdvancedPaste_FL_LocalModel" /><LineBreak /><Run
|
||||||
x:Uid="AdvancedPaste_FL_UseCLIToDownloadModels"
|
x:Uid="AdvancedPaste_FL_UseCliToDownloadModels"
|
||||||
FontSize="12"
|
FontSize="12"
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
@@ -152,7 +170,7 @@
|
|||||||
Spacing="8">
|
Spacing="8">
|
||||||
<Image Width="36" Source="ms-appx:///Assets/Settings/Icons/Models/FoundryLocal.svg" />
|
<Image Width="36" Source="ms-appx:///Assets/Settings/Icons/Models/FoundryLocal.svg" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
x:Uid="AdvancedPaste_FL_FLNotavailableYet"
|
x:Uid="AdvancedPaste_FL_FLNotAvailableYet"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
TextAlignment="Center"
|
TextAlignment="Center"
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public sealed partial class FoundryLocalModelPicker : UserControl
|
|||||||
|
|
||||||
public delegate void DownloadRequestedEventHandler(object sender, object payload);
|
public delegate void DownloadRequestedEventHandler(object sender, object payload);
|
||||||
|
|
||||||
public delegate void LoadRequestedEventHandler(object sender, FoundryLoadRequestedEventArgs args);
|
public delegate void LoadRequestedEventHandler(object sender);
|
||||||
|
|
||||||
public event ModelSelectionChangedEventHandler SelectionChanged;
|
public event ModelSelectionChangedEventHandler SelectionChanged;
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ public sealed partial class FoundryLocalModelPicker : UserControl
|
|||||||
|
|
||||||
public bool HasDownloadableModels => DownloadableModels?.Cast<object>().Any() ?? false;
|
public bool HasDownloadableModels => DownloadableModels?.Cast<object>().Any() ?? false;
|
||||||
|
|
||||||
public void RequestLoad(bool refresh)
|
public void RequestLoad()
|
||||||
{
|
{
|
||||||
if (IsLoading)
|
if (IsLoading)
|
||||||
{
|
{
|
||||||
@@ -107,7 +107,7 @@ public sealed partial class FoundryLocalModelPicker : UserControl
|
|||||||
|
|
||||||
IsAvailable = false;
|
IsAvailable = false;
|
||||||
StatusText = "Loading Foundry Local status...";
|
StatusText = "Loading Foundry Local status...";
|
||||||
LoadRequested?.Invoke(this, new FoundryLoadRequestedEventArgs(refresh));
|
LoadRequested?.Invoke(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OnCachedModelsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
private static void OnCachedModelsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
@@ -310,7 +310,7 @@ public sealed partial class FoundryLocalModelPicker : UserControl
|
|||||||
|
|
||||||
private void RefreshModelsButton_Click(object sender, RoutedEventArgs e)
|
private void RefreshModelsButton_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
RequestLoad(refresh: true);
|
RequestLoad();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateVisualStates()
|
private void UpdateVisualStates()
|
||||||
@@ -444,14 +444,4 @@ public sealed partial class FoundryLocalModelPicker : UserControl
|
|||||||
{
|
{
|
||||||
return string.IsNullOrWhiteSpace(license) ? Visibility.Collapsed : Visibility.Visible;
|
return string.IsNullOrWhiteSpace(license) ? Visibility.Collapsed : Visibility.Visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class FoundryLoadRequestedEventArgs : EventArgs
|
|
||||||
{
|
|
||||||
public FoundryLoadRequestedEventArgs(bool refresh)
|
|
||||||
{
|
|
||||||
Refresh = refresh;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Refresh { get; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
|||||||
{
|
{
|
||||||
ViewModel.RefreshEnabledState();
|
ViewModel.RefreshEnabledState();
|
||||||
UpdatePasteAIUIVisibility();
|
UpdatePasteAIUIVisibility();
|
||||||
_ = UpdateFoundryLocalUIAsync(refreshFoundry: true);
|
_ = UpdateFoundryLocalUIAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnableAdvancedPasteAI() => ViewModel.EnableAI();
|
private void EnableAdvancedPasteAI() => ViewModel.EnableAI();
|
||||||
@@ -384,7 +384,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task UpdateFoundryLocalUIAsync(bool refreshFoundry = false)
|
private Task UpdateFoundryLocalUIAsync()
|
||||||
{
|
{
|
||||||
string selectedType = ViewModel?.PasteAIProviderDraft?.ServiceType ?? string.Empty;
|
string selectedType = ViewModel?.PasteAIProviderDraft?.ServiceType ?? string.Empty;
|
||||||
bool isFoundryLocal = string.Equals(selectedType, "FoundryLocal", StringComparison.OrdinalIgnoreCase);
|
bool isFoundryLocal = string.Equals(selectedType, "FoundryLocal", StringComparison.OrdinalIgnoreCase);
|
||||||
@@ -419,12 +419,12 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
|||||||
PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = false;
|
PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
FoundryLocalPicker?.RequestLoad(refreshFoundry);
|
FoundryLocalPicker?.RequestLoad();
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadFoundryLocalModelsAsync(bool refresh = false)
|
private async Task LoadFoundryLocalModelsAsync()
|
||||||
{
|
{
|
||||||
if (FoundryLocalPanel is null)
|
if (FoundryLocalPanel is null)
|
||||||
{
|
{
|
||||||
@@ -456,9 +456,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
IEnumerable<ModelDetails> cachedModelsEnumerable = refresh
|
IEnumerable<ModelDetails> cachedModelsEnumerable = await provider.GetModelsAsync(cancelationToken: cancellationToken).ConfigureAwait(false);
|
||||||
? await provider.GetModelsAsync(ignoreCached: true, cancelationToken: cancellationToken)
|
|
||||||
: await provider.GetModelsAsync(cancelationToken: cancellationToken);
|
|
||||||
|
|
||||||
if (cancellationToken.IsCancellationRequested)
|
if (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
@@ -467,9 +465,12 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
|||||||
|
|
||||||
var cachedModels = cachedModelsEnumerable?.ToList() ?? new List<ModelDetails>();
|
var cachedModels = cachedModelsEnumerable?.ToList() ?? new List<ModelDetails>();
|
||||||
|
|
||||||
|
DispatcherQueue.TryEnqueue(() =>
|
||||||
|
{
|
||||||
UpdateFoundryCollections(cachedModels);
|
UpdateFoundryCollections(cachedModels);
|
||||||
ShowFoundryAvailableState();
|
ShowFoundryAvailableState();
|
||||||
RestoreFoundrySelection(cachedModels);
|
RestoreFoundrySelection(cachedModels);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -478,12 +479,18 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
var errorMessage = $"Unable to load Foundry Local models. {ex.Message}";
|
var errorMessage = $"Unable to load Foundry Local models. {ex.Message}";
|
||||||
ShowFoundryUnavailableState(errorMessage);
|
|
||||||
System.Diagnostics.Debug.WriteLine($"[AdvancedPastePage] Failed to load Foundry Local models: {ex}");
|
System.Diagnostics.Debug.WriteLine($"[AdvancedPastePage] Failed to load Foundry Local models: {ex}");
|
||||||
|
DispatcherQueue.TryEnqueue(() =>
|
||||||
|
{
|
||||||
|
ShowFoundryUnavailableState(errorMessage);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
{
|
||||||
|
DispatcherQueue.TryEnqueue(() =>
|
||||||
{
|
{
|
||||||
UpdateFoundrySaveButtonState();
|
UpdateFoundrySaveButtonState();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,9 +679,9 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
|||||||
UpdateFoundrySaveButtonState();
|
UpdateFoundrySaveButtonState();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void FoundryLocalPicker_LoadRequested(object sender, FoundryLocalModelPicker.FoundryLoadRequestedEventArgs args)
|
private async void FoundryLocalPicker_LoadRequested(object sender)
|
||||||
{
|
{
|
||||||
await LoadFoundryLocalModelsAsync(args?.Refresh ?? false);
|
await LoadFoundryLocalModelsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class FoundryDownloadableModel : INotifyPropertyChanged
|
private sealed class FoundryDownloadableModel : INotifyPropertyChanged
|
||||||
@@ -1089,7 +1096,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
|||||||
PasteAIProviderConfigurationDialog.Title = $"{displayName} provider configuration";
|
PasteAIProviderConfigurationDialog.Title = $"{displayName} provider configuration";
|
||||||
}
|
}
|
||||||
|
|
||||||
await UpdateFoundryLocalUIAsync(refreshFoundry: true);
|
await UpdateFoundryLocalUIAsync();
|
||||||
UpdatePasteAIUIVisibility();
|
UpdatePasteAIUIVisibility();
|
||||||
RefreshDialogBindings();
|
RefreshDialogBindings();
|
||||||
|
|
||||||
@@ -1118,7 +1125,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
|||||||
: $"{titlePrefix} provider configuration";
|
: $"{titlePrefix} provider configuration";
|
||||||
|
|
||||||
UpdatePasteAIUIVisibility();
|
UpdatePasteAIUIVisibility();
|
||||||
await UpdateFoundryLocalUIAsync(refreshFoundry: false);
|
await UpdateFoundryLocalUIAsync();
|
||||||
RefreshDialogBindings();
|
RefreshDialogBindings();
|
||||||
PasteAIApiKeyPasswordBox.Password = ViewModel.GetPasteAIApiKey(provider.Id, provider.ServiceType);
|
PasteAIApiKeyPasswordBox.Password = ViewModel.GetPasteAIApiKey(provider.Id, provider.ServiceType);
|
||||||
await PasteAIProviderConfigurationDialog.ShowAsync();
|
await PasteAIProviderConfigurationDialog.ShowAsync();
|
||||||
|
|||||||
@@ -10,40 +10,202 @@
|
|||||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||||
AutomationProperties.LandmarkType="Main"
|
AutomationProperties.LandmarkType="Main"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
|
<Grid>
|
||||||
|
<ScrollViewer AutomationProperties.AutomationId="PageScrollViewer">
|
||||||
|
<Grid
|
||||||
|
MaxWidth="1000"
|
||||||
|
Padding="16,0,16,0"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||||
|
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="16"
|
||||||
|
RowSpacing="8">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<tkcontrols:OpacityMaskView Margin="-16,0,-16,0" HorizontalAlignment="Stretch">
|
||||||
|
<tkcontrols:OpacityMaskView.OpacityMask>
|
||||||
|
<Rectangle>
|
||||||
|
<Rectangle.Fill>
|
||||||
|
<LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
|
||||||
|
<GradientStop Offset="0.50" Color="Black" />
|
||||||
|
<GradientStop Offset="0.75" Color="#80000000" />
|
||||||
|
<GradientStop Offset="0.95" Color="Transparent" />
|
||||||
|
</LinearGradientBrush>
|
||||||
|
</Rectangle.Fill>
|
||||||
|
</Rectangle>
|
||||||
|
</tkcontrols:OpacityMaskView.OpacityMask>
|
||||||
|
<Grid Height="560">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Image
|
||||||
|
Grid.RowSpan="3"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Source="/Assets/Settings/Modules/CmdPal_Background.png"
|
||||||
|
Stretch="UniformToFill" />
|
||||||
|
<TextBlock
|
||||||
|
Margin="0,24,0,12"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
FontSize="36"
|
||||||
|
FontWeight="Bold"
|
||||||
|
Text="Command Palette">
|
||||||
|
<TextBlock.Foreground>
|
||||||
|
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
|
||||||
|
<GradientStop Offset="0.0" Color="#FFB9EBFF" />
|
||||||
|
<GradientStop Offset="0.49" Color="#FF86CBFF" />
|
||||||
|
<GradientStop Offset="1.0" Color="#FFA1E7FF" />
|
||||||
|
</LinearGradientBrush>
|
||||||
|
</TextBlock.Foreground>
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="1"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Foreground="White"
|
||||||
|
TextAlignment="Center"
|
||||||
|
TextWrapping="Wrap">
|
||||||
|
<Run x:Uid="CmdPal_Description" />
|
||||||
|
<Hyperlink NavigateUri="">
|
||||||
|
<Run x:Uid="LearnMore_CmdPal.Text" Foreground="White" />
|
||||||
|
</Hyperlink>
|
||||||
|
</TextBlock>
|
||||||
|
<Image
|
||||||
|
Grid.Row="2"
|
||||||
|
Margin="0,16,0,0"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Source="/Assets/Settings/Modules/CmdPal_Hero.png"
|
||||||
|
Stretch="Uniform" />
|
||||||
|
</Grid>
|
||||||
|
</tkcontrols:OpacityMaskView>
|
||||||
|
|
||||||
<controls:SettingsPageControl x:Uid="CmdPal" ModuleImageSource="ms-appx:///Assets/Settings/Modules/CmdPal.png">
|
<Grid
|
||||||
<controls:SettingsPageControl.ModuleContent>
|
Grid.Row="1"
|
||||||
<StackPanel ChildrenTransitions="{StaticResource SettingsCardsAnimations}" Orientation="Vertical">
|
Margin="0,-12,0,24"
|
||||||
|
ColumnSpacing="32"
|
||||||
|
RowSpacing="8">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<FontIcon
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Glyph="" />
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="1"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
TextAlignment="Center"
|
||||||
|
TextWrapping="Wrap">
|
||||||
|
<Run x:Uid="CmdPal_ExtensibleHeader" FontWeight="SemiBold" /> <LineBreak />
|
||||||
|
<Run
|
||||||
|
x:Uid="CmdPal_ExtensibleDescription"
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
|
<FontIcon
|
||||||
|
Grid.Column="1"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Glyph="" />
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="1"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
TextAlignment="Center"
|
||||||
|
TextWrapping="Wrap">
|
||||||
|
<Run x:Uid="CmdPal_FastHeader" FontWeight="SemiBold" /> <LineBreak />
|
||||||
|
<Run
|
||||||
|
x:Uid="CmdPal_FastDescription"
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
|
<FontIcon
|
||||||
|
Grid.Column="2"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Glyph="" />
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="2"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
TextAlignment="Center"
|
||||||
|
TextWrapping="Wrap">
|
||||||
|
<Run x:Uid="CmdPal_ModernHeader" FontWeight="SemiBold" /> <LineBreak />
|
||||||
|
<Run
|
||||||
|
x:Uid="CmdPal_ModernDescription"
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||||
|
</TextBlock>
|
||||||
|
</Grid>
|
||||||
|
<StackPanel
|
||||||
|
Grid.Row="2"
|
||||||
|
Margin="0,8,0,0"
|
||||||
|
Orientation="Vertical"
|
||||||
|
Spacing="{StaticResource SettingsCardSpacing}">
|
||||||
<controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsEnabledGpoConfigured, Mode=OneWay}">
|
<controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsEnabledGpoConfigured, Mode=OneWay}">
|
||||||
<tkcontrols:SettingsCard
|
<tkcontrols:SettingsCard
|
||||||
Name="CmdPalEnableCmdPal"
|
Name="CmdPalEnableCmdPal"
|
||||||
x:Uid="CmdPal_Enable_CmdPal"
|
x:Uid="CmdPal_Enable_CmdPal"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/CmdPal.png}">
|
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/CmdPal.png}">
|
||||||
<ToggleSwitch IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" />
|
<ToggleSwitch IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" />
|
||||||
</tkcontrols:SettingsCard>
|
</tkcontrols:SettingsCard>
|
||||||
</controls:GPOInfoControl>
|
</controls:GPOInfoControl>
|
||||||
<controls:SettingsGroup x:Uid="CmdPal_Activation_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
|
||||||
<tkcontrols:SettingsCard
|
<tkcontrols:SettingsCard
|
||||||
Name="CmdPalActivationShortcut"
|
x:Uid="CmdPal_Launch"
|
||||||
x:Uid="CmdPal_ActivationShortcut"
|
Grid.Row="3"
|
||||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
ActionIcon="{ui:FontIcon Glyph=}"
|
||||||
<controls:ShortcutControl
|
Click="LaunchCard_Click"
|
||||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
Header="Launch Command Palette"
|
||||||
HotkeySettings="{x:Bind Path=ViewModel.Hotkey, Mode=OneWay}"
|
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||||
IsEnabled="False" />
|
IsClickEnabled="True"
|
||||||
<tkcontrols:SettingsCard.Description>
|
IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
||||||
<HyperlinkButton
|
<ItemsControl
|
||||||
x:Name="CmdPalSettingsDeeplink"
|
AutomationProperties.AccessibilityView="Raw"
|
||||||
x:Uid="CmdPal_DeeplinkContent"
|
IsTabStop="False"
|
||||||
Click="CmdPalSettingsDeeplink_Click" />
|
ItemsSource="{x:Bind Path=ViewModel.Hotkey.GetKeysList(), Mode=OneWay}">
|
||||||
</tkcontrols:SettingsCard.Description>
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="4" />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<controls:KeyVisual
|
||||||
|
Padding="8,8,8,8"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
AutomationProperties.AccessibilityView="Raw"
|
||||||
|
Content="{Binding}"
|
||||||
|
Style="{StaticResource AccentKeyVisualStyle}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
</tkcontrols:SettingsCard>
|
</tkcontrols:SettingsCard>
|
||||||
</controls:SettingsGroup>
|
<tkcontrols:SettingsCard
|
||||||
|
x:Uid="CmdPal_Settings"
|
||||||
|
Grid.Row="4"
|
||||||
|
ActionIcon="{ui:FontIcon Glyph=}"
|
||||||
|
Click="SettingsCard_Click"
|
||||||
|
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||||
|
IsClickEnabled="True"
|
||||||
|
IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</controls:SettingsPageControl.ModuleContent>
|
</Grid>
|
||||||
|
</ScrollViewer>
|
||||||
<controls:SettingsPageControl.PrimaryLinks>
|
</Grid>
|
||||||
<controls:PageLink x:Uid="LearnMore_CmdPal" Link="https://aka.ms/PowerToysOverview_CmdPal" />
|
|
||||||
</controls:SettingsPageControl.PrimaryLinks>
|
|
||||||
</controls:SettingsPageControl>
|
|
||||||
</local:NavigablePage>
|
</local:NavigablePage>
|
||||||
|
|||||||
@@ -63,12 +63,20 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CmdPalSettingsDeeplink_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
private void SettingsCard_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
// Launch CmdPal settings window as normal user using explorer
|
// Launch CmdPal settings window as normal user using explorer
|
||||||
string launchPath = "explorer.exe";
|
string launchPath = "explorer.exe";
|
||||||
string launchArgs = "x-cmdpal://settings";
|
string launchArgs = "x-cmdpal://settings";
|
||||||
LaunchApp(launchPath, launchArgs);
|
LaunchApp(launchPath, launchArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void LaunchCard_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
// Launch CmdPal window as normal user using explorer
|
||||||
|
string launchPath = "explorer.exe";
|
||||||
|
string launchArgs = "x-cmdpal:";
|
||||||
|
LaunchApp(launchPath, launchArgs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2703,7 +2703,7 @@ From there, simply click on one of the supported files in the File Explorer and
|
|||||||
<value>Mouse Pointer Crosshairs</value>
|
<value>Mouse Pointer Crosshairs</value>
|
||||||
<comment>Mouse as in the hardware peripheral.</comment>
|
<comment>Mouse as in the hardware peripheral.</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="Oobe_MouseUtils_MousePointerCrosshairs.Description" xml:space="preserve">
|
<data name="Oobe_MouseUtils_MousePointerCrosshairs_Description.Text" xml:space="preserve">
|
||||||
<value>Draw crosshairs centered around the mouse pointer.</value>
|
<value>Draw crosshairs centered around the mouse pointer.</value>
|
||||||
<comment>Mouse as in the hardware peripheral.</comment>
|
<comment>Mouse as in the hardware peripheral.</comment>
|
||||||
</data>
|
</data>
|
||||||
@@ -2827,7 +2827,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
|||||||
<comment>Refers to the utility name</comment>
|
<comment>Refers to the utility name</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="MouseUtils_FindMyMouse.Description" xml:space="preserve">
|
<data name="MouseUtils_FindMyMouse.Description" xml:space="preserve">
|
||||||
<value>Find My Mouse highlights the position of the cursor when pressing the Ctrl key twice, using a custom shortcut or when shaking the mouse.</value>
|
<value>Highlight the position of the cursor when pressing the Ctrl key twice, using a custom shortcut or when shaking the mouse.</value>
|
||||||
<comment>"Ctrl" is a keyboard key. "Find My Mouse" is the name of the utility</comment>
|
<comment>"Ctrl" is a keyboard key. "Find My Mouse" is the name of the utility</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="MouseUtils_Enable_FindMyMouse.Header" xml:space="preserve">
|
<data name="MouseUtils_Enable_FindMyMouse.Header" xml:space="preserve">
|
||||||
@@ -2916,7 +2916,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
|||||||
<comment>Refers to the utility name</comment>
|
<comment>Refers to the utility name</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="MouseUtils_MouseHighlighter.Description" xml:space="preserve">
|
<data name="MouseUtils_MouseHighlighter.Description" xml:space="preserve">
|
||||||
<value>Mouse Highlighter mode will highlight mouse clicks.</value>
|
<value>Highlight mouse clicks.</value>
|
||||||
<comment>"Mouse Highlighter" is the name of the utility. Mouse is the hardware mouse.</comment>
|
<comment>"Mouse Highlighter" is the name of the utility. Mouse is the hardware mouse.</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="MouseUtils_Enable_MouseHighlighter.Header" xml:space="preserve">
|
<data name="MouseUtils_Enable_MouseHighlighter.Header" xml:space="preserve">
|
||||||
@@ -2961,7 +2961,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
|||||||
<comment>Refers to the utility name</comment>
|
<comment>Refers to the utility name</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="MouseUtils_MousePointerCrosshairs.Description" xml:space="preserve">
|
<data name="MouseUtils_MousePointerCrosshairs.Description" xml:space="preserve">
|
||||||
<value>Mouse Pointer Crosshairs draws crosshairs centered on the mouse pointer.</value>
|
<value>Draw crosshairs centered on the mouse pointer.</value>
|
||||||
<comment>"Mouse Pointer Crosshairs" is the name of the utility. Mouse is the hardware mouse.</comment>
|
<comment>"Mouse Pointer Crosshairs" is the name of the utility. Mouse is the hardware mouse.</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="MouseUtils_Enable_MousePointerCrosshairs.Header" xml:space="preserve">
|
<data name="MouseUtils_Enable_MousePointerCrosshairs.Header" xml:space="preserve">
|
||||||
@@ -3410,7 +3410,7 @@ Activate by holding the key for the character you want to add an accent to, then
|
|||||||
<value>An AI powered tool to put your clipboard content into any format you need, focused towards developer workflows.</value>
|
<value>An AI powered tool to put your clipboard content into any format you need, focused towards developer workflows.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AdvancedPaste_EnableAISettingsCardDescription.Text" xml:space="preserve">
|
<data name="AdvancedPaste_EnableAISettingsCardDescription.Text" xml:space="preserve">
|
||||||
<value>Transform your clipboard content with the power of AI. An cloud or local endpoint is required.</value>
|
<value>Transform your clipboard content with the power of AI. A cloud or local endpoint is required.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AdvancedPaste_EnableAISettingsCardDescriptionLearnMore.Content" xml:space="preserve">
|
<data name="AdvancedPaste_EnableAISettingsCardDescriptionLearnMore.Content" xml:space="preserve">
|
||||||
<value>Learn more</value>
|
<value>Learn more</value>
|
||||||
@@ -5159,25 +5159,12 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
|
|||||||
<data name="Shell_TopLevelSystemTools.Content" xml:space="preserve">
|
<data name="Shell_TopLevelSystemTools.Content" xml:space="preserve">
|
||||||
<value>System Tools</value>
|
<value>System Tools</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="CmdPal.ModuleTitle" xml:space="preserve">
|
|
||||||
<value>Command Palette</value>
|
|
||||||
</data>
|
|
||||||
<data name="CmdPal_ShortDescription" xml:space="preserve">
|
|
||||||
<value>A better quick launcher</value>
|
|
||||||
</data>
|
|
||||||
<data name="CmdPal_ActivationDescription" xml:space="preserve">
|
|
||||||
<value>Open Command Palette</value>
|
|
||||||
</data>
|
|
||||||
<data name="CmdPal_Enable_CmdPal.Header" xml:space="preserve">
|
<data name="CmdPal_Enable_CmdPal.Header" xml:space="preserve">
|
||||||
<value>Enable Command Palette</value>
|
<value>Enable Command Palette</value>
|
||||||
<comment>"Command Palette" is the name of the utility.</comment>
|
<comment>Command Palette is a product name, do not loc</comment>
|
||||||
</data>
|
|
||||||
<data name="CmdPal.ModuleDescription" xml:space="preserve">
|
|
||||||
<value>A fully extensible quick launcher with a richer display and additional capabilities without sacrificing performance.</value>
|
|
||||||
</data>
|
</data>
|
||||||
<data name="LearnMore_CmdPal.Text" xml:space="preserve">
|
<data name="LearnMore_CmdPal.Text" xml:space="preserve">
|
||||||
<value>Learn more about Command Palette</value>
|
<value>Learn more</value>
|
||||||
<comment>Command Palette is a product name, do not loc</comment>
|
|
||||||
</data>
|
</data>
|
||||||
<data name="Shell_CmdPal.Content" xml:space="preserve">
|
<data name="Shell_CmdPal.Content" xml:space="preserve">
|
||||||
<value>Command Palette</value>
|
<value>Command Palette</value>
|
||||||
@@ -5185,11 +5172,11 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
|
|||||||
</data>
|
</data>
|
||||||
<data name="Oobe_CmdPal.Description" xml:space="preserve">
|
<data name="Oobe_CmdPal.Description" xml:space="preserve">
|
||||||
<value>A fully extensible quick launcher with a richer display and additional capabilities without sacrificing performance.</value>
|
<value>A fully extensible quick launcher with a richer display and additional capabilities without sacrificing performance.</value>
|
||||||
<comment>"Command Palette" is a product name</comment>
|
<comment>Command Palette is a product name, do not loc</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="Oobe_CmdPal.Title" xml:space="preserve">
|
<data name="Oobe_CmdPal.Title" xml:space="preserve">
|
||||||
<value>Command Palette</value>
|
<value>Command Palette</value>
|
||||||
<comment>"Command Palette" is a product name</comment>
|
<comment>Command Palette is a product name, do not loc</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="Oobe_CmdPal_HowToUse.Text" xml:space="preserve">
|
<data name="Oobe_CmdPal_HowToUse.Text" xml:space="preserve">
|
||||||
<value>and start typing!</value>
|
<value>and start typing!</value>
|
||||||
@@ -5222,14 +5209,8 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
|
|||||||
<data name="RetryLabel.Text" xml:space="preserve">
|
<data name="RetryLabel.Text" xml:space="preserve">
|
||||||
<value>Retry</value>
|
<value>Retry</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="CmdPal_Activation_GroupSettings.Header" xml:space="preserve">
|
<data name="CmdPal_Settings.Header" xml:space="preserve">
|
||||||
<value>Activation</value>
|
<value>Settings</value>
|
||||||
</data>
|
|
||||||
<data name="CmdPal_ActivationShortcut.Header" xml:space="preserve">
|
|
||||||
<value>Activation shortcut</value>
|
|
||||||
</data>
|
|
||||||
<data name="CmdPal_DeeplinkContent.Content" xml:space="preserve">
|
|
||||||
<value>Open Command Palette settings to customize the activation shortcut</value>
|
|
||||||
</data>
|
</data>
|
||||||
<data name="Help_chromaCIE" xml:space="preserve">
|
<data name="Help_chromaCIE" xml:space="preserve">
|
||||||
<value>chroma (CIE LCh)</value>
|
<value>chroma (CIE LCh)</value>
|
||||||
@@ -5693,14 +5674,14 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
|
|||||||
<value>Foundry Local model</value>
|
<value>Foundry Local model</value>
|
||||||
<comment>Do not localize "Foundry Local", it's a product name</comment>
|
<comment>Do not localize "Foundry Local", it's a product name</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="AdvancedPaste_FL_UseCLIToDownloadModels.Text" xml:space="preserve">
|
<data name="AdvancedPaste_FL_UseCliToDownloadModels.Text" xml:space="preserve">
|
||||||
<value>Use the Foundry Local CLI to download models that run locally on-device. They'll appear here.</value>
|
<value>Use the Foundry Local CLI to download models that run locally on-device. They'll appear here.</value>
|
||||||
<comment>Do not localize "Foundry Local", it's a product name</comment>
|
<comment>Do not localize "Foundry Local", it's a product name</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="AdvancedPaste_FL_RefreshModelList.Text" xml:space="preserve">
|
<data name="AdvancedPaste_FL_RefreshModelList.Text" xml:space="preserve">
|
||||||
<value>Refresh model list</value>
|
<value>Refresh model list</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AdvancedPaste_FL_FLNotavailableYet.Text" xml:space="preserve">
|
<data name="AdvancedPaste_FL_FLNotAvailableYet.Text" xml:space="preserve">
|
||||||
<value>Foundry Local is not available on this device yet.</value>
|
<value>Foundry Local is not available on this device yet.</value>
|
||||||
<comment>Do not localize "Foundry Local", it's a product name</comment>
|
<comment>Do not localize "Foundry Local", it's a product name</comment>
|
||||||
</data>
|
</data>
|
||||||
@@ -5753,4 +5734,51 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
|
|||||||
<data name="AdvancedPaste_EnableClipboardPreview.Description" xml:space="preserve">
|
<data name="AdvancedPaste_EnableClipboardPreview.Description" xml:space="preserve">
|
||||||
<value>Display a preview of the current clipboard content</value>
|
<value>Display a preview of the current clipboard content</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="AdvancedPaste_FL_LearnMoreFoundryLocal.Content" xml:space="preserve">
|
||||||
|
<value>Learn more</value>
|
||||||
|
</data>
|
||||||
|
<data name="AdvancedPaste_FL_PreviewMessage.Message" xml:space="preserve">
|
||||||
|
<value>Foundry Local is still in public preview</value>
|
||||||
|
<comment>Do not loc "Foundry Local"</comment>
|
||||||
|
</data>
|
||||||
|
<data name="CmdPal_Settings.Description" xml:space="preserve">
|
||||||
|
<value>Configure the activation shortcut, extensions, behavior and much more</value>
|
||||||
|
</data>
|
||||||
|
<data name="CmdPal_Launch.Header" xml:space="preserve">
|
||||||
|
<value>Open Command Palette</value>
|
||||||
|
<comment>Command Palette is a product name, do not loc</comment>
|
||||||
|
</data>
|
||||||
|
<data name="CmdPal_ShortDescription" xml:space="preserve">
|
||||||
|
<value>A better quick launcher</value>
|
||||||
|
</data>
|
||||||
|
<data name="CmdPal_Description.Text" xml:space="preserve">
|
||||||
|
<value>Find files, launch apps, and do so much more with the most extensible quick launcher.</value>
|
||||||
|
</data>
|
||||||
|
<data name="CmdPal.ModuleTitle" xml:space="preserve">
|
||||||
|
<value>Command Palette</value>
|
||||||
|
<comment>Command Palette is a product name, do not loc</comment>
|
||||||
|
</data>
|
||||||
|
<data name="CmdPal_ActivationDescription" xml:space="preserve">
|
||||||
|
<value>Open Command Palette</value>
|
||||||
|
<comment>Command Palette is a product name, do not loc</comment>
|
||||||
|
</data>
|
||||||
|
<data name="CmdPal_ExtensibleDescription.Text" xml:space="preserve">
|
||||||
|
<value>Powerful extensions help you do more</value>
|
||||||
|
</data>
|
||||||
|
<data name="CmdPal_ExtensibleHeader.Text" xml:space="preserve">
|
||||||
|
<value>Extensible</value>
|
||||||
|
</data>
|
||||||
|
<data name="CmdPal_FastDescription.Text" xml:space="preserve">
|
||||||
|
<value>Find files and launch apps in an instant</value>
|
||||||
|
</data>
|
||||||
|
<data name="CmdPal_FastHeader.Text" xml:space="preserve">
|
||||||
|
<value>Fast</value>
|
||||||
|
</data>
|
||||||
|
<data name="CmdPal_ModernHeader.Text" xml:space="preserve">
|
||||||
|
<value>Beautiful</value>
|
||||||
|
</data>
|
||||||
|
<data name="CmdPal_ModernDescription.Text" xml:space="preserve">
|
||||||
|
<value>A modern UI built with Fluent Design</value>
|
||||||
|
<comment>Fluent Design is a product name, do not loc</comment>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -40,6 +40,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
|||||||
|
|
||||||
public ObservableCollection<DashboardListItem> ActionModules { get; set; } = new ObservableCollection<DashboardListItem>();
|
public ObservableCollection<DashboardListItem> ActionModules { get; set; } = new ObservableCollection<DashboardListItem>();
|
||||||
|
|
||||||
|
// Master list of module items that is sorted and projected into AllModules.
|
||||||
|
private List<DashboardListItem> _moduleItems = new List<DashboardListItem>();
|
||||||
|
|
||||||
|
// Flag to prevent circular updates when a UI toggle triggers settings changes.
|
||||||
|
private bool _isUpdatingFromUI;
|
||||||
|
|
||||||
private AllHotkeyConflictsData _allHotkeyConflictsData = new AllHotkeyConflictsData();
|
private AllHotkeyConflictsData _allHotkeyConflictsData = new AllHotkeyConflictsData();
|
||||||
|
|
||||||
public AllHotkeyConflictsData AllHotkeyConflictsData
|
public AllHotkeyConflictsData AllHotkeyConflictsData
|
||||||
@@ -74,7 +80,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
|||||||
generalSettingsConfig.DashboardSortOrder = value;
|
generalSettingsConfig.DashboardSortOrder = value;
|
||||||
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(generalSettingsConfig);
|
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(generalSettingsConfig);
|
||||||
SendConfigMSG(outgoing.ToString());
|
SendConfigMSG(outgoing.ToString());
|
||||||
RefreshModuleList();
|
SortModuleList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,8 +102,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
|||||||
// set the callback functions value to handle outgoing IPC message.
|
// set the callback functions value to handle outgoing IPC message.
|
||||||
SendConfigMSG = ipcMSGCallBackFunc;
|
SendConfigMSG = ipcMSGCallBackFunc;
|
||||||
|
|
||||||
RefreshModuleList();
|
BuildModuleList();
|
||||||
GetShortcutModules();
|
SortModuleList();
|
||||||
|
RefreshShortcutModules();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnConflictsUpdated(object sender, AllHotkeyConflictsEventArgs e)
|
protected override void OnConflictsUpdated(object sender, AllHotkeyConflictsEventArgs e)
|
||||||
@@ -129,11 +136,13 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
|||||||
GlobalHotkeyConflictManager.Instance?.RequestAllConflicts();
|
GlobalHotkeyConflictManager.Instance?.RequestAllConflicts();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RefreshModuleList()
|
/// <summary>
|
||||||
|
/// Builds the master list of module items. Called once during initialization.
|
||||||
|
/// Each module item contains its configuration, enabled state, and GPO lock status.
|
||||||
|
/// </summary>
|
||||||
|
private void BuildModuleList()
|
||||||
{
|
{
|
||||||
AllModules.Clear();
|
_moduleItems.Clear();
|
||||||
|
|
||||||
var moduleItems = new List<DashboardListItem>();
|
|
||||||
|
|
||||||
foreach (ModuleType moduleType in Enum.GetValues<ModuleType>())
|
foreach (ModuleType moduleType in Enum.GetValues<ModuleType>())
|
||||||
{
|
{
|
||||||
@@ -149,23 +158,94 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
|||||||
DashboardModuleItems = GetModuleItems(moduleType),
|
DashboardModuleItems = GetModuleItems(moduleType),
|
||||||
};
|
};
|
||||||
newItem.EnabledChangedCallback = EnabledChangedOnUI;
|
newItem.EnabledChangedCallback = EnabledChangedOnUI;
|
||||||
moduleItems.Add(newItem);
|
_moduleItems.Add(newItem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort based on current sort order
|
/// <summary>
|
||||||
var sortedItems = DashboardSortOrder switch
|
/// Sorts the module list according to the current sort order and updates the AllModules collection.
|
||||||
|
/// On first call, populates AllModules. On subsequent calls, uses Move() to reorder items in-place
|
||||||
|
/// to avoid destroying and recreating UI elements.
|
||||||
|
/// </summary>
|
||||||
|
private void SortModuleList()
|
||||||
{
|
{
|
||||||
DashboardSortOrder.ByStatus => moduleItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label),
|
var sortedItems = (DashboardSortOrder switch
|
||||||
_ => moduleItems.OrderBy(x => x.Label), // Default alphabetical
|
{
|
||||||
};
|
DashboardSortOrder.ByStatus => _moduleItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label),
|
||||||
|
_ => _moduleItems.OrderBy(x => x.Label), // Default alphabetical
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
// If AllModules is empty (first load), just populate it.
|
||||||
|
if (AllModules.Count == 0)
|
||||||
|
{
|
||||||
foreach (var item in sortedItems)
|
foreach (var item in sortedItems)
|
||||||
{
|
{
|
||||||
AllModules.Add(item);
|
AllModules.Add(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Otherwise, update the collection in place using Move to avoid UI glitches.
|
||||||
|
for (int i = 0; i < sortedItems.Count; i++)
|
||||||
|
{
|
||||||
|
var currentItem = sortedItems[i];
|
||||||
|
var currentIndex = AllModules.IndexOf(currentItem);
|
||||||
|
|
||||||
|
if (currentIndex != -1 && currentIndex != i)
|
||||||
|
{
|
||||||
|
AllModules.Move(currentIndex, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify that DashboardSortOrder changed so the menu updates its checked state.
|
||||||
|
OnPropertyChanged(nameof(DashboardSortOrder));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Refreshes module enabled/locked states by re-reading GPO configuration. Only
|
||||||
|
/// updates properties that have actually changed to minimize UI notifications
|
||||||
|
/// then re-sorts the list according to the current sort order.
|
||||||
|
/// </summary>
|
||||||
|
private void RefreshModuleList()
|
||||||
|
{
|
||||||
|
foreach (var item in _moduleItems)
|
||||||
|
{
|
||||||
|
GpoRuleConfigured gpo = ModuleHelper.GetModuleGpoConfiguration(item.Tag);
|
||||||
|
|
||||||
|
// GPO can force-enable (Enabled) or force-disable (Disabled) a module.
|
||||||
|
// If Enabled: module is on and the user cannot disable it.
|
||||||
|
// If Disabled: module is off and the user cannot enable it.
|
||||||
|
// Otherwise, the setting is unlocked and the user can enable/disable it.
|
||||||
|
bool newEnabledState = gpo == GpoRuleConfigured.Enabled || (gpo != GpoRuleConfigured.Disabled && ModuleHelper.GetIsModuleEnabled(generalSettingsConfig, item.Tag));
|
||||||
|
|
||||||
|
// Lock the toggle when GPO is controlling the module.
|
||||||
|
bool newLockedState = gpo == GpoRuleConfigured.Enabled || gpo == GpoRuleConfigured.Disabled;
|
||||||
|
|
||||||
|
// Only update if there's an actual change to minimize UI notifications.
|
||||||
|
if (item.IsEnabled != newEnabledState)
|
||||||
|
{
|
||||||
|
item.IsEnabled = newEnabledState;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.IsLocked != newLockedState)
|
||||||
|
{
|
||||||
|
item.IsLocked = newLockedState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SortModuleList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Callback invoked when a user toggles a module's enabled state in the UI.
|
||||||
|
/// Sets the _isUpdatingFromUI flag to prevent circular updates, then updates
|
||||||
|
/// settings, re-sorts if needed, and refreshes dependent collections.
|
||||||
|
/// </summary>
|
||||||
private void EnabledChangedOnUI(DashboardListItem dashboardListItem)
|
private void EnabledChangedOnUI(DashboardListItem dashboardListItem)
|
||||||
|
{
|
||||||
|
_isUpdatingFromUI = true;
|
||||||
|
try
|
||||||
{
|
{
|
||||||
Views.ShellPage.UpdateGeneralSettingsCallback(dashboardListItem.Tag, dashboardListItem.IsEnabled);
|
Views.ShellPage.UpdateGeneralSettingsCallback(dashboardListItem.Tag, dashboardListItem.IsEnabled);
|
||||||
|
|
||||||
@@ -176,20 +256,45 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
|||||||
NewPlusViewModel.CopyTemplateExamples(settings.Properties.TemplateLocation.Value);
|
NewPlusViewModel.CopyTemplateExamples(settings.Properties.TemplateLocation.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request updated conflicts after module state change
|
// Re-sort only required if sorting by enabled status.
|
||||||
RequestConflictData();
|
if (DashboardSortOrder == DashboardSortOrder.ByStatus)
|
||||||
|
{
|
||||||
|
SortModuleList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always refresh shortcuts/actions to reflect enabled state changes.
|
||||||
|
RefreshShortcutModules();
|
||||||
|
|
||||||
|
// Request updated conflicts after module state change.
|
||||||
|
RequestConflictData();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isUpdatingFromUI = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Callback invoked when module enabled state changes from other parts of the
|
||||||
|
/// settings UI. Ignores the notification if it was triggered by a UI toggle
|
||||||
|
/// we're already handling, to prevent circular updates.
|
||||||
|
/// </summary>
|
||||||
public void ModuleEnabledChangedOnSettingsPage()
|
public void ModuleEnabledChangedOnSettingsPage()
|
||||||
{
|
{
|
||||||
|
// Ignore if this was triggered by a UI change that we're already handling.
|
||||||
|
if (_isUpdatingFromUI)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
RefreshModuleList();
|
RefreshModuleList();
|
||||||
GetShortcutModules();
|
RefreshShortcutModules();
|
||||||
|
|
||||||
OnPropertyChanged(nameof(ShortcutModules));
|
OnPropertyChanged(nameof(ShortcutModules));
|
||||||
|
|
||||||
// Request updated conflicts after module state change
|
// Request updated conflicts after module state change.
|
||||||
RequestConflictData();
|
RequestConflictData();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -198,7 +303,11 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void GetShortcutModules()
|
/// <summary>
|
||||||
|
/// Rebuilds ShortcutModules and ActionModules collections by filtering AllModules
|
||||||
|
/// to only include enabled modules and their respective shortcut/action items.
|
||||||
|
/// </summary>
|
||||||
|
private void RefreshShortcutModules()
|
||||||
{
|
{
|
||||||
ShortcutModules.Clear();
|
ShortcutModules.Clear();
|
||||||
ActionModules.Clear();
|
ActionModules.Clear();
|
||||||
|
|||||||
37
tools/module_loader/ModuleLoader.manifest
Normal file
37
tools/module_loader/ModuleLoader.manifest
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||||
|
<assemblyIdentity
|
||||||
|
version="1.0.0.0"
|
||||||
|
processorArchitecture="*"
|
||||||
|
name="Microsoft.PowerToys.ModuleLoader"
|
||||||
|
type="win32"
|
||||||
|
/>
|
||||||
|
<description>PowerToys Module Loader - Standalone module testing utility</description>
|
||||||
|
|
||||||
|
<!-- Per-Monitor DPI Awareness V2 -->
|
||||||
|
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<windowsSettings>
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||||
|
</windowsSettings>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
<!-- Request administrator execution level if needed -->
|
||||||
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||||
|
<security>
|
||||||
|
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||||
|
</requestedPrivileges>
|
||||||
|
</security>
|
||||||
|
</trustInfo>
|
||||||
|
|
||||||
|
<!-- Windows 10+ compatibility -->
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<!-- Windows 10 -->
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||||
|
<!-- Windows 11 -->
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9b}"/>
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
</assembly>
|
||||||
205
tools/module_loader/ModuleLoader.vcxproj
Normal file
205
tools/module_loader/ModuleLoader.vcxproj
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<ItemGroup Label="ProjectConfigurations">
|
||||||
|
<ProjectConfiguration Include="Debug|x64">
|
||||||
|
<Configuration>Debug</Configuration>
|
||||||
|
<Platform>x64</Platform>
|
||||||
|
</ProjectConfiguration>
|
||||||
|
<ProjectConfiguration Include="Debug|ARM64">
|
||||||
|
<Configuration>Debug</Configuration>
|
||||||
|
<Platform>ARM64</Platform>
|
||||||
|
</ProjectConfiguration>
|
||||||
|
<ProjectConfiguration Include="Release|x64">
|
||||||
|
<Configuration>Release</Configuration>
|
||||||
|
<Platform>x64</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>{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}</ProjectGuid>
|
||||||
|
<RootNamespace>ModuleLoader</RootNamespace>
|
||||||
|
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||||
|
<ProjectName>ModuleLoader</ProjectName>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
||||||
|
<ConfigurationType>Application</ConfigurationType>
|
||||||
|
<UseDebugLibraries>true</UseDebugLibraries>
|
||||||
|
<PlatformToolset>v143</PlatformToolset>
|
||||||
|
<CharacterSet>Unicode</CharacterSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" Label="Configuration">
|
||||||
|
<ConfigurationType>Application</ConfigurationType>
|
||||||
|
<UseDebugLibraries>true</UseDebugLibraries>
|
||||||
|
<PlatformToolset>v143</PlatformToolset>
|
||||||
|
<CharacterSet>Unicode</CharacterSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
|
||||||
|
<ConfigurationType>Application</ConfigurationType>
|
||||||
|
<UseDebugLibraries>false</UseDebugLibraries>
|
||||||
|
<PlatformToolset>v143</PlatformToolset>
|
||||||
|
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||||
|
<CharacterSet>Unicode</CharacterSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="Configuration">
|
||||||
|
<ConfigurationType>Application</ConfigurationType>
|
||||||
|
<UseDebugLibraries>false</UseDebugLibraries>
|
||||||
|
<PlatformToolset>v143</PlatformToolset>
|
||||||
|
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||||
|
<CharacterSet>Unicode</CharacterSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||||
|
<ImportGroup Label="ExtensionSettings">
|
||||||
|
</ImportGroup>
|
||||||
|
<ImportGroup Label="Shared">
|
||||||
|
</ImportGroup>
|
||||||
|
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|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|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|ARM64'">
|
||||||
|
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||||
|
</ImportGroup>
|
||||||
|
<PropertyGroup Label="UserMacros" />
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||||
|
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
|
||||||
|
<IntDir>$(Platform)\$(Configuration)\</IntDir>
|
||||||
|
<TargetName>ModuleLoader</TargetName>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
|
||||||
|
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
|
||||||
|
<IntDir>$(Platform)\$(Configuration)\</IntDir>
|
||||||
|
<TargetName>ModuleLoader</TargetName>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||||
|
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
|
||||||
|
<IntDir>$(Platform)\$(Configuration)\</IntDir>
|
||||||
|
<TargetName>ModuleLoader</TargetName>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
|
||||||
|
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
|
||||||
|
<IntDir>$(Platform)\$(Configuration)\</IntDir>
|
||||||
|
<TargetName>ModuleLoader</TargetName>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||||
|
<ClCompile>
|
||||||
|
<WarningLevel>Level4</WarningLevel>
|
||||||
|
<SDLCheck>true</SDLCheck>
|
||||||
|
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||||
|
<ConformanceMode>true</ConformanceMode>
|
||||||
|
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||||
|
<AdditionalIncludeDirectories>$(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||||
|
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||||
|
<TreatWarningAsError>false</TreatWarningAsError>
|
||||||
|
</ClCompile>
|
||||||
|
<Link>
|
||||||
|
<SubSystem>Console</SubSystem>
|
||||||
|
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||||
|
<AdditionalDependencies>kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||||
|
<AdditionalManifestDependencies>type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'</AdditionalManifestDependencies>
|
||||||
|
</Link>
|
||||||
|
<Manifest>
|
||||||
|
<AdditionalManifestFiles>$(ProjectDir)ModuleLoader.manifest</AdditionalManifestFiles>
|
||||||
|
</Manifest>
|
||||||
|
</ItemDefinitionGroup>
|
||||||
|
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
|
||||||
|
<ClCompile>
|
||||||
|
<WarningLevel>Level4</WarningLevel>
|
||||||
|
<SDLCheck>true</SDLCheck>
|
||||||
|
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||||
|
<ConformanceMode>true</ConformanceMode>
|
||||||
|
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||||
|
<AdditionalIncludeDirectories>$(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||||
|
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||||
|
<TreatWarningAsError>false</TreatWarningAsError>
|
||||||
|
</ClCompile>
|
||||||
|
<Link>
|
||||||
|
<SubSystem>Console</SubSystem>
|
||||||
|
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||||
|
<AdditionalDependencies>kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||||
|
<AdditionalManifestDependencies>type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'</AdditionalManifestDependencies>
|
||||||
|
</Link>
|
||||||
|
<Manifest>
|
||||||
|
<AdditionalManifestFiles>$(ProjectDir)ModuleLoader.manifest</AdditionalManifestFiles>
|
||||||
|
</Manifest>
|
||||||
|
</ItemDefinitionGroup>
|
||||||
|
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||||
|
<ClCompile>
|
||||||
|
<WarningLevel>Level4</WarningLevel>
|
||||||
|
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||||
|
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||||
|
<SDLCheck>true</SDLCheck>
|
||||||
|
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||||
|
<ConformanceMode>true</ConformanceMode>
|
||||||
|
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||||
|
<AdditionalIncludeDirectories>$(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||||
|
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||||
|
<TreatWarningAsError>false</TreatWarningAsError>
|
||||||
|
</ClCompile>
|
||||||
|
<Link>
|
||||||
|
<SubSystem>Console</SubSystem>
|
||||||
|
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||||
|
<OptimizeReferences>true</OptimizeReferences>
|
||||||
|
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||||
|
<AdditionalDependencies>kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||||
|
<AdditionalManifestDependencies>type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'</AdditionalManifestDependencies>
|
||||||
|
</Link>
|
||||||
|
<Manifest>
|
||||||
|
<AdditionalManifestFiles>$(ProjectDir)ModuleLoader.manifest</AdditionalManifestFiles>
|
||||||
|
</Manifest>
|
||||||
|
</ItemDefinitionGroup>
|
||||||
|
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
|
||||||
|
<ClCompile>
|
||||||
|
<WarningLevel>Level4</WarningLevel>
|
||||||
|
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||||
|
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||||
|
<SDLCheck>true</SDLCheck>
|
||||||
|
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||||
|
<ConformanceMode>true</ConformanceMode>
|
||||||
|
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||||
|
<AdditionalIncludeDirectories>$(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||||
|
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||||
|
<TreatWarningAsError>false</TreatWarningAsError>
|
||||||
|
</ClCompile>
|
||||||
|
<Link>
|
||||||
|
<SubSystem>Console</SubSystem>
|
||||||
|
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||||
|
<OptimizeReferences>true</OptimizeReferences>
|
||||||
|
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||||
|
<AdditionalDependencies>kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||||
|
<AdditionalManifestDependencies>type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'</AdditionalManifestDependencies>
|
||||||
|
</Link>
|
||||||
|
<Manifest>
|
||||||
|
<AdditionalManifestFiles>$(ProjectDir)ModuleLoader.manifest</AdditionalManifestFiles>
|
||||||
|
</Manifest>
|
||||||
|
</ItemDefinitionGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ClCompile Include="src\main.cpp" />
|
||||||
|
<ClCompile Include="src\ModuleLoader.cpp" />
|
||||||
|
<ClCompile Include="src\SettingsLoader.cpp" />
|
||||||
|
<ClCompile Include="src\HotkeyManager.cpp" />
|
||||||
|
<ClCompile Include="src\ConsoleHost.cpp" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ClInclude Include="src\ModuleLoader.h" />
|
||||||
|
<ClInclude Include="src\SettingsLoader.h" />
|
||||||
|
<ClInclude Include="src\HotkeyManager.h" />
|
||||||
|
<ClInclude Include="src\ConsoleHost.h" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="README.md" />
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||||
|
<ImportGroup Label="ExtensionTargets">
|
||||||
|
</ImportGroup>
|
||||||
|
</Project>
|
||||||
51
tools/module_loader/ModuleLoader.vcxproj.filters
Normal file
51
tools/module_loader/ModuleLoader.vcxproj.filters
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?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>
|
||||||
|
<ClCompile Include="src\main.cpp">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
|
<ClCompile Include="src\ModuleLoader.cpp">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
|
<ClCompile Include="src\SettingsLoader.cpp">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
|
<ClCompile Include="src\HotkeyManager.cpp">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
|
<ClCompile Include="src\ConsoleHost.cpp">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ClInclude Include="src\ModuleLoader.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
|
<ClInclude Include="src\SettingsLoader.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
|
<ClInclude Include="src\HotkeyManager.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
|
<ClInclude Include="src\ConsoleHost.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="README.md" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
483
tools/module_loader/SHARING.md
Normal file
483
tools/module_loader/SHARING.md
Normal file
@@ -0,0 +1,483 @@
|
|||||||
|
# Sharing ModuleLoader and Modules
|
||||||
|
|
||||||
|
This guide explains how to share the ModuleLoader tool and PowerToy modules with others for testing purposes.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The ModuleLoader is designed to be a **portable, standalone testing tool** that can be shared with module developers and testers. It has minimal dependencies and can work with any compatible PowerToy module DLL.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What You Need to Share
|
||||||
|
|
||||||
|
### For Testing a Module (e.g., CursorWrap)
|
||||||
|
|
||||||
|
#### **Minimum Package** (Recommended for Quick Testing)
|
||||||
|
|
||||||
|
1. **ModuleLoader.exe** - The standalone loader application
|
||||||
|
- Location: `x64\Debug\ModuleLoader.exe` or `x64\Release\ModuleLoader.exe`
|
||||||
|
- No additional DLLs required (uses only Windows system libraries)
|
||||||
|
|
||||||
|
2. **The Module DLL** - The PowerToy module to test
|
||||||
|
- Example: `CursorWrap.dll` from `x64\Debug\` or `x64\Release\`
|
||||||
|
- Location varies by module (see module-specific locations below)
|
||||||
|
|
||||||
|
3. **settings.json** - Module configuration (place in same folder as the DLL)
|
||||||
|
- **NEW**: Settings can be placed alongside the module DLL for portable testing
|
||||||
|
- Location: Same directory as the module DLL (e.g., `settings.json` next to `CursorWrap.dll`)
|
||||||
|
- Falls back to: `%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\settings.json` if not found locally
|
||||||
|
|
||||||
|
#### **Complete Standalone Package** (For Users Without PowerToys Installed)
|
||||||
|
|
||||||
|
1. **ModuleLoader.exe**
|
||||||
|
2. **Module DLL**
|
||||||
|
3. **Sample settings.json** - Pre-configured settings file
|
||||||
|
4. **Installation instructions** - See "Standalone Package Setup" section below
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Debug Builds
|
||||||
|
If you build the module in Debug configuration:
|
||||||
|
- The module will output debug messages via `OutputDebugString()`
|
||||||
|
- View these with [DebugView](https://learn.microsoft.com/sysinternals/downloads/debugview) or Visual Studio Output window
|
||||||
|
- Example: CursorWrap outputs detailed topology and cursor wrapping debug info
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Module-Specific File Locations
|
||||||
|
|
||||||
|
### CursorWrap
|
||||||
|
```
|
||||||
|
Files to share:
|
||||||
|
- x64\Debug\CursorWrap.dll (or Release)
|
||||||
|
- %LOCALAPPDATA%\Microsoft\PowerToys\CursorWrap\settings.json
|
||||||
|
|
||||||
|
Size: ~100KB
|
||||||
|
```
|
||||||
|
|
||||||
|
### MouseHighlighter
|
||||||
|
```
|
||||||
|
Files to share:
|
||||||
|
- x64\Debug\MouseHighlighter.dll (or Release)
|
||||||
|
- %LOCALAPPDATA%\Microsoft\PowerToys\MouseHighlighter\settings.json
|
||||||
|
|
||||||
|
Size: ~150KB
|
||||||
|
```
|
||||||
|
|
||||||
|
### FindMyMouse
|
||||||
|
```
|
||||||
|
Files to share:
|
||||||
|
- x64\Debug\FindMyMouse.dll (or Release)
|
||||||
|
- %LOCALAPPDATA%\Microsoft\PowerToys\FindMyMouse\settings.json
|
||||||
|
|
||||||
|
Size: ~120KB
|
||||||
|
```
|
||||||
|
|
||||||
|
### MousePointerCrosshairs
|
||||||
|
```
|
||||||
|
Files to share:
|
||||||
|
- x64\Debug\MousePointerCrosshairs.dll (or Release)
|
||||||
|
- %LOCALAPPDATA%\Microsoft\PowerToys\MousePointerCrosshairs\settings.json
|
||||||
|
|
||||||
|
Size: ~140KB
|
||||||
|
```
|
||||||
|
|
||||||
|
### MouseJump
|
||||||
|
```
|
||||||
|
Files to share:
|
||||||
|
- x64\Debug\MouseJump.dll (or Release)
|
||||||
|
- %LOCALAPPDATA%\Microsoft\PowerToys\MouseJump\settings.json
|
||||||
|
|
||||||
|
Note: MouseJump is a UI-based module and may not work fully with ModuleLoader
|
||||||
|
Size: ~200KB
|
||||||
|
```
|
||||||
|
|
||||||
|
### AlwaysOnTop
|
||||||
|
```
|
||||||
|
Files to share:
|
||||||
|
- x64\Debug\AlwaysOnTop.dll (or Release)
|
||||||
|
- %LOCALAPPDATA%\Microsoft\PowerToys\AlwaysOnTop\settings.json
|
||||||
|
|
||||||
|
Size: ~100KB
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependency Analysis
|
||||||
|
|
||||||
|
### ModuleLoader.exe Dependencies
|
||||||
|
**Windows System Libraries Only** (automatically available on all Windows systems):
|
||||||
|
- `KERNEL32.dll` - Core Windows API
|
||||||
|
- `USER32.dll` - User interface functions
|
||||||
|
- `SHELL32.dll` - Shell functions
|
||||||
|
- `ole32.dll` - COM library
|
||||||
|
|
||||||
|
**No PowerToys dependencies required!** The ModuleLoader is completely standalone.
|
||||||
|
|
||||||
|
### Module DLL Dependencies (Typical)
|
||||||
|
Most PowerToy modules depend on:
|
||||||
|
- Windows system DLLs (automatically available)
|
||||||
|
- PowerToys common libraries (if any, they're typically statically linked)
|
||||||
|
- **Module settings** - Must be present in `%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\`
|
||||||
|
|
||||||
|
**Important**: Modules are generally **self-contained** and statically link most dependencies. You typically only need the module DLL itself.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Creating a Standalone Package
|
||||||
|
|
||||||
|
### Step 1: Prepare the Files
|
||||||
|
|
||||||
|
Create a folder structure like this:
|
||||||
|
```
|
||||||
|
ModuleLoaderPackage\
|
||||||
|
??? ModuleLoader.exe
|
||||||
|
??? CursorWrap.dll (or other module)
|
||||||
|
??? settings.json (module settings - placed locally!)
|
||||||
|
```
|
||||||
|
|
||||||
|
**NEW Simplified Structure**: You can now place `settings.json` directly alongside the module DLL! The ModuleLoader will check this location first before looking in the standard PowerToys settings directories.
|
||||||
|
|
||||||
|
### Step 2: Extract Settings from Your Machine
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Copy settings from your development machine
|
||||||
|
$moduleName = "CursorWrap" # Change as needed
|
||||||
|
$settingsPath = "$env:LOCALAPPDATA\Microsoft\PowerToys\$moduleName\settings.json"
|
||||||
|
Copy-Item $settingsPath ".\settings\$moduleName\settings.json"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Create Installation Instructions (README.txt)
|
||||||
|
|
||||||
|
```text
|
||||||
|
PowerToys Module Testing Package
|
||||||
|
=================================
|
||||||
|
|
||||||
|
This package contains the ModuleLoader tool for testing PowerToy modules.
|
||||||
|
|
||||||
|
Contents:
|
||||||
|
- ModuleLoader.exe : Standalone module loader
|
||||||
|
- modules\*.dll : PowerToy module(s) to test
|
||||||
|
- settings\*\*.json : Module configuration files
|
||||||
|
|
||||||
|
Setup (First Time):
|
||||||
|
-------------------
|
||||||
|
1. Create settings directory:
|
||||||
|
%LOCALAPPDATA%\Microsoft\PowerToys\
|
||||||
|
|
||||||
|
2. Copy settings:
|
||||||
|
Copy the entire "settings\<ModuleName>" folder to:
|
||||||
|
%LOCALAPPDATA%\Microsoft\PowerToys\
|
||||||
|
|
||||||
|
Example for CursorWrap:
|
||||||
|
Copy "settings\CursorWrap" to:
|
||||||
|
%LOCALAPPDATA%\Microsoft\PowerToys\CursorWrap\
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
------
|
||||||
|
ModuleLoader.exe modules\CursorWrap.dll
|
||||||
|
|
||||||
|
The tool will:
|
||||||
|
- Load the module DLL
|
||||||
|
- Read settings from %LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\
|
||||||
|
- Register hotkeys
|
||||||
|
- Enable the module
|
||||||
|
|
||||||
|
Press Ctrl+C to exit.
|
||||||
|
Press the module's hotkey to toggle functionality.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
-------------
|
||||||
|
- Windows 10 1803 or later
|
||||||
|
- No PowerToys installation required!
|
||||||
|
|
||||||
|
Troubleshooting:
|
||||||
|
----------------
|
||||||
|
If you see "Settings file not found":
|
||||||
|
1. Make sure you copied the settings folder correctly
|
||||||
|
2. Check that the path is:
|
||||||
|
%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\settings.json
|
||||||
|
3. You can also run PowerToys once to generate default settings
|
||||||
|
|
||||||
|
Debug Logs:
|
||||||
|
-----------
|
||||||
|
Module logs are written to:
|
||||||
|
%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\Logs\
|
||||||
|
|
||||||
|
For debug builds, use DebugView to see real-time output.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Distribution Methods
|
||||||
|
|
||||||
|
### Method 1: ZIP Archive
|
||||||
|
```powershell
|
||||||
|
# Create a complete package
|
||||||
|
$moduleName = "CursorWrap"
|
||||||
|
$packageName = "ModuleLoader-$moduleName-Package"
|
||||||
|
|
||||||
|
# Collect files
|
||||||
|
New-Item $packageName -ItemType Directory
|
||||||
|
Copy-Item "x64\Debug\ModuleLoader.exe" "$packageName\"
|
||||||
|
New-Item "$packageName\modules" -ItemType Directory
|
||||||
|
Copy-Item "x64\Debug\$moduleName.dll" "$packageName\modules\"
|
||||||
|
New-Item "$packageName\settings\$moduleName" -ItemType Directory -Force
|
||||||
|
Copy-Item "$env:LOCALAPPDATA\Microsoft\PowerToys\$moduleName\settings.json" "$packageName\settings\$moduleName\"
|
||||||
|
|
||||||
|
# Create README
|
||||||
|
@"
|
||||||
|
See README in the tools\module_loader folder for instructions
|
||||||
|
"@ | Out-File "$packageName\README.txt"
|
||||||
|
|
||||||
|
# Zip it
|
||||||
|
Compress-Archive -Path $packageName -DestinationPath "$packageName.zip"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method 2: Direct Share (Advanced Users)
|
||||||
|
For developers who already have PowerToys installed:
|
||||||
|
```powershell
|
||||||
|
# Just share the executables
|
||||||
|
Copy-Item "x64\Debug\ModuleLoader.exe" "\\ShareLocation\"
|
||||||
|
Copy-Item "x64\Debug\CursorWrap.dll" "\\ShareLocation\"
|
||||||
|
```
|
||||||
|
|
||||||
|
They can run: `ModuleLoader.exe CursorWrap.dll`
|
||||||
|
(Settings will be loaded from their existing PowerToys installation)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Platform-Specific Notes
|
||||||
|
|
||||||
|
### x64 vs ARM64
|
||||||
|
|
||||||
|
**Important**: Match architectures!
|
||||||
|
- `x64\Debug\ModuleLoader.exe` ? Only works with `x64` module DLLs
|
||||||
|
- `ARM64\Debug\ModuleLoader.exe` ? Only works with `ARM64` module DLLs
|
||||||
|
|
||||||
|
**Distribution Tip**: Provide both architectures if targeting multiple platforms:
|
||||||
|
```
|
||||||
|
ModuleLoaderPackage\
|
||||||
|
??? x64\
|
||||||
|
? ??? ModuleLoader.exe
|
||||||
|
? ??? modules\CursorWrap.dll
|
||||||
|
??? ARM64\
|
||||||
|
? ??? ModuleLoader.exe
|
||||||
|
? ??? modules\CursorWrap.dll
|
||||||
|
??? settings\...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debug vs Release
|
||||||
|
|
||||||
|
**Debug builds**:
|
||||||
|
- Larger file size
|
||||||
|
- Include debug symbols
|
||||||
|
- Verbose logging via `OutputDebugString()`
|
||||||
|
- Recommended for testing/development
|
||||||
|
|
||||||
|
**Release builds**:
|
||||||
|
- Smaller file size
|
||||||
|
- Optimized performance
|
||||||
|
- Minimal logging
|
||||||
|
- Recommended for end-user testing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
Before sharing a module package:
|
||||||
|
|
||||||
|
- [ ] ModuleLoader.exe is included
|
||||||
|
- [ ] Module DLL is included (matching architecture)
|
||||||
|
- [ ] Sample settings.json is included
|
||||||
|
- [ ] README/instructions are included
|
||||||
|
- [ ] Tested on a clean machine (no PowerToys installed)
|
||||||
|
- [ ] Verified hotkeys work
|
||||||
|
- [ ] Verified Ctrl+C exits cleanly
|
||||||
|
- [ ] Confirmed settings path in documentation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced: Portable Package Script
|
||||||
|
|
||||||
|
Here's a complete PowerShell script to create a fully portable package:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[string]$ModuleName,
|
||||||
|
|
||||||
|
[ValidateSet("Debug", "Release")]
|
||||||
|
[string]$Configuration = "Debug",
|
||||||
|
|
||||||
|
[ValidateSet("x64", "ARM64")]
|
||||||
|
[string]$Platform = "x64"
|
||||||
|
)
|
||||||
|
|
||||||
|
$packageName = "ModuleLoader-$ModuleName-$Platform-$Configuration"
|
||||||
|
$packagePath = ".\$packageName"
|
||||||
|
|
||||||
|
Write-Host "Creating portable package: $packageName" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Create structure
|
||||||
|
New-Item $packagePath -ItemType Directory -Force | Out-Null
|
||||||
|
New-Item "$packagePath\modules" -ItemType Directory -Force | Out-Null
|
||||||
|
New-Item "$packagePath\settings\$ModuleName" -ItemType Directory -Force | Out-Null
|
||||||
|
|
||||||
|
# Copy ModuleLoader
|
||||||
|
$loaderPath = "$Platform\$Configuration\ModuleLoader.exe"
|
||||||
|
if (Test-Path $loaderPath) {
|
||||||
|
Copy-Item $loaderPath "$packagePath\"
|
||||||
|
Write-Host "? Copied ModuleLoader.exe" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "? ModuleLoader.exe not found at $loaderPath" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Copy Module DLL
|
||||||
|
$modulePath = "$Platform\$Configuration\$ModuleName.dll"
|
||||||
|
if (Test-Path $modulePath) {
|
||||||
|
Copy-Item $modulePath "$packagePath\modules\"
|
||||||
|
Write-Host "? Copied $ModuleName.dll" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "? $ModuleName.dll not found at $modulePath" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Copy Settings
|
||||||
|
$settingsPath = "$env:LOCALAPPDATA\Microsoft\PowerToys\$ModuleName\settings.json"
|
||||||
|
if (Test-Path $settingsPath) {
|
||||||
|
Copy-Item $settingsPath "$packagePath\settings\$ModuleName\"
|
||||||
|
Write-Host "? Copied settings.json" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "? Settings not found at $settingsPath - creating placeholder" -ForegroundColor Yellow
|
||||||
|
@"
|
||||||
|
{
|
||||||
|
"name": "$ModuleName",
|
||||||
|
"version": "1.0"
|
||||||
|
}
|
||||||
|
"@ | Out-File "$packagePath\settings\$ModuleName\settings.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create README
|
||||||
|
@"
|
||||||
|
PowerToys $ModuleName Testing Package
|
||||||
|
======================================
|
||||||
|
|
||||||
|
Configuration: $Configuration
|
||||||
|
Platform: $Platform
|
||||||
|
|
||||||
|
Setup Instructions:
|
||||||
|
-------------------
|
||||||
|
1. Copy the 'settings\$ModuleName' folder to:
|
||||||
|
%LOCALAPPDATA%\Microsoft\PowerToys\
|
||||||
|
|
||||||
|
2. Run:
|
||||||
|
ModuleLoader.exe modules\$ModuleName.dll
|
||||||
|
|
||||||
|
3. Press Ctrl+C to exit
|
||||||
|
|
||||||
|
Logs are written to:
|
||||||
|
%LOCALAPPDATA%\Microsoft\PowerToys\$ModuleName\Logs\
|
||||||
|
|
||||||
|
For more information, see:
|
||||||
|
https://github.com/microsoft/PowerToys/tree/main/tools/module_loader
|
||||||
|
"@ | Out-File "$packagePath\README.txt"
|
||||||
|
|
||||||
|
# Create ZIP
|
||||||
|
$zipPath = "$packageName.zip"
|
||||||
|
Compress-Archive -Path $packagePath -DestinationPath $zipPath -Force
|
||||||
|
Write-Host "? Created $zipPath" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Show summary
|
||||||
|
Write-Host "`nPackage Contents:" -ForegroundColor Cyan
|
||||||
|
Get-ChildItem $packagePath -Recurse | ForEach-Object {
|
||||||
|
Write-Host " $($_.FullName.Replace($packagePath, ''))"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`nPackage ready: $zipPath" -ForegroundColor Green
|
||||||
|
Write-Host "Size: $([math]::Round((Get-Item $zipPath).Length / 1KB, 2)) KB"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage**:
|
||||||
|
```powershell
|
||||||
|
.\CreateModulePackage.ps1 -ModuleName "CursorWrap" -Configuration Release -Platform x64
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### Q: Can I share just ModuleLoader.exe and the module DLL?
|
||||||
|
**A**: Yes, but the recipient must have PowerToys installed (or manually create the settings file).
|
||||||
|
|
||||||
|
### Q: Does the tester need PowerToys installed?
|
||||||
|
**A**: No, if you provide the complete package with settings. ModuleLoader is fully standalone.
|
||||||
|
|
||||||
|
### Q: What if settings.json doesn't exist?
|
||||||
|
**A**: ModuleLoader will show an error. Either:
|
||||||
|
1. Run PowerToys once with the module enabled to generate settings
|
||||||
|
2. Manually create a minimal settings.json file
|
||||||
|
3. Include a sample settings.json in your package
|
||||||
|
|
||||||
|
### Q: Can I test modules on a virtual machine?
|
||||||
|
**A**: Yes! This is a great use case. Just copy the package to the VM - no PowerToys installation needed.
|
||||||
|
|
||||||
|
### Q: Do I need to include PDB files?
|
||||||
|
**A**: Only for debugging. For normal testing, just the EXE and DLL are sufficient.
|
||||||
|
|
||||||
|
### Q: Can I distribute this to end users?
|
||||||
|
**A**: ModuleLoader is a **development/testing tool**, not intended for end-user distribution. For production use, direct users to install PowerToys.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
When sharing module DLLs:
|
||||||
|
|
||||||
|
1. **Verify Source**: Only share modules you built from trusted source code
|
||||||
|
2. **Scan for Malware**: Run antivirus scans on the package before sharing
|
||||||
|
3. **HTTPS Only**: Use secure channels (HTTPS, OneDrive, SharePoint) for distribution
|
||||||
|
4. **Hash Verification**: Consider providing SHA256 hashes for file integrity:
|
||||||
|
```powershell
|
||||||
|
Get-FileHash ModuleLoader.exe -Algorithm SHA256
|
||||||
|
Get-FileHash modules\CursorWrap.dll -Algorithm SHA256
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example Package (CursorWrap)
|
||||||
|
|
||||||
|
Here's what a complete CursorWrap testing package looks like:
|
||||||
|
|
||||||
|
```
|
||||||
|
ModuleLoader-CursorWrap-x64-Debug.zip (220 KB)
|
||||||
|
?
|
||||||
|
??? ModuleLoader-CursorWrap-x64-Debug\
|
||||||
|
??? ModuleLoader.exe (160 KB)
|
||||||
|
??? README.txt (2 KB)
|
||||||
|
??? modules\
|
||||||
|
? ??? CursorWrap.dll (55 KB)
|
||||||
|
??? settings\
|
||||||
|
??? CursorWrap\
|
||||||
|
??? settings.json (3 KB)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Total package size**: ~220 KB (compressed)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues with ModuleLoader, see:
|
||||||
|
- [ModuleLoader README](./README.md)
|
||||||
|
- [PowerToys Documentation](https://aka.ms/PowerToysOverview)
|
||||||
|
- [PowerToys GitHub Issues](https://github.com/microsoft/PowerToys/issues)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
ModuleLoader is part of PowerToys and is licensed under the MIT License.
|
||||||
|
See the LICENSE file in the PowerToys repository root for details.
|
||||||
80
tools/module_loader/src/ConsoleHost.cpp
Normal file
80
tools/module_loader/src/ConsoleHost.cpp
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
#include "ConsoleHost.h"
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
bool ConsoleHost::s_exitRequested = false;
|
||||||
|
|
||||||
|
ConsoleHost::ConsoleHost(ModuleLoader& moduleLoader, HotkeyManager& hotkeyManager)
|
||||||
|
: m_moduleLoader(moduleLoader)
|
||||||
|
, m_hotkeyManager(hotkeyManager)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
ConsoleHost::~ConsoleHost()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL WINAPI ConsoleHost::ConsoleCtrlHandler(DWORD ctrlType)
|
||||||
|
{
|
||||||
|
switch (ctrlType)
|
||||||
|
{
|
||||||
|
case CTRL_C_EVENT:
|
||||||
|
case CTRL_BREAK_EVENT:
|
||||||
|
case CTRL_CLOSE_EVENT:
|
||||||
|
std::wcout << L"\nCtrl+C received, shutting down...\n";
|
||||||
|
s_exitRequested = true;
|
||||||
|
|
||||||
|
// Post a quit message to break the message loop
|
||||||
|
PostQuitMessage(0);
|
||||||
|
return TRUE;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConsoleHost::Run()
|
||||||
|
{
|
||||||
|
// Install console control handler
|
||||||
|
if (!SetConsoleCtrlHandler(ConsoleCtrlHandler, TRUE))
|
||||||
|
{
|
||||||
|
std::wcerr << L"Warning: Failed to set console control handler\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
s_exitRequested = false;
|
||||||
|
|
||||||
|
// Message loop
|
||||||
|
MSG msg;
|
||||||
|
while (!s_exitRequested)
|
||||||
|
{
|
||||||
|
// Wait for a message with a timeout so we can check s_exitRequested
|
||||||
|
DWORD result = MsgWaitForMultipleObjects(0, nullptr, FALSE, 100, QS_ALLINPUT);
|
||||||
|
|
||||||
|
if (result == WAIT_OBJECT_0)
|
||||||
|
{
|
||||||
|
// Process all pending messages
|
||||||
|
while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
|
||||||
|
{
|
||||||
|
if (msg.message == WM_QUIT)
|
||||||
|
{
|
||||||
|
s_exitRequested = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.message == WM_HOTKEY)
|
||||||
|
{
|
||||||
|
m_hotkeyManager.HandleHotkey(static_cast<int>(msg.wParam), m_moduleLoader);
|
||||||
|
}
|
||||||
|
|
||||||
|
TranslateMessage(&msg);
|
||||||
|
DispatchMessage(&msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove console control handler
|
||||||
|
SetConsoleCtrlHandler(ConsoleCtrlHandler, FALSE);
|
||||||
|
}
|
||||||
38
tools/module_loader/src/ConsoleHost.h
Normal file
38
tools/module_loader/src/ConsoleHost.h
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Windows.h>
|
||||||
|
#include "ModuleLoader.h"
|
||||||
|
#include "HotkeyManager.h"
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Console host that runs the message loop and handles Ctrl+C
|
||||||
|
/// </summary>
|
||||||
|
class ConsoleHost
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ConsoleHost(ModuleLoader& moduleLoader, HotkeyManager& hotkeyManager);
|
||||||
|
~ConsoleHost();
|
||||||
|
|
||||||
|
// Prevent copying
|
||||||
|
ConsoleHost(const ConsoleHost&) = delete;
|
||||||
|
ConsoleHost& operator=(const ConsoleHost&) = delete;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Run the message loop until Ctrl+C is pressed
|
||||||
|
/// </summary>
|
||||||
|
void Run();
|
||||||
|
|
||||||
|
private:
|
||||||
|
ModuleLoader& m_moduleLoader;
|
||||||
|
HotkeyManager& m_hotkeyManager;
|
||||||
|
static bool s_exitRequested;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Console control handler (for Ctrl+C)
|
||||||
|
/// </summary>
|
||||||
|
static BOOL WINAPI ConsoleCtrlHandler(DWORD ctrlType);
|
||||||
|
};
|
||||||
279
tools/module_loader/src/HotkeyManager.cpp
Normal file
279
tools/module_loader/src/HotkeyManager.cpp
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
#include "HotkeyManager.h"
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
HotkeyManager::HotkeyManager()
|
||||||
|
: m_nextHotkeyId(1) // Start from 1
|
||||||
|
, m_hotkeyExRegistered(false)
|
||||||
|
, m_hotkeyExId(0)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
HotkeyManager::~HotkeyManager()
|
||||||
|
{
|
||||||
|
UnregisterAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
UINT HotkeyManager::ConvertModifiers(bool win, bool ctrl, bool alt, bool shift) const
|
||||||
|
{
|
||||||
|
UINT modifiers = MOD_NOREPEAT; // Prevent repeat events
|
||||||
|
if (win) modifiers |= MOD_WIN;
|
||||||
|
if (ctrl) modifiers |= MOD_CONTROL;
|
||||||
|
if (alt) modifiers |= MOD_ALT;
|
||||||
|
if (shift) modifiers |= MOD_SHIFT;
|
||||||
|
return modifiers;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HotkeyManager::RegisterModuleHotkeys(ModuleLoader& moduleLoader)
|
||||||
|
{
|
||||||
|
if (!moduleLoader.IsLoaded())
|
||||||
|
{
|
||||||
|
std::wcerr << L"Error: Module not loaded\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool anyRegistered = false;
|
||||||
|
|
||||||
|
// First, try the newer GetHotkeyEx() API
|
||||||
|
auto hotkeyEx = moduleLoader.GetHotkeyEx();
|
||||||
|
if (hotkeyEx.has_value())
|
||||||
|
{
|
||||||
|
std::wcout << L"Module has HotkeyEx activation hotkey\n";
|
||||||
|
|
||||||
|
UINT modifiers = hotkeyEx->modifiersMask | MOD_NOREPEAT;
|
||||||
|
UINT vkCode = hotkeyEx->vkCode;
|
||||||
|
|
||||||
|
if (vkCode != 0)
|
||||||
|
{
|
||||||
|
int hotkeyId = m_nextHotkeyId++;
|
||||||
|
|
||||||
|
std::wcout << L" Registering HotkeyEx: ";
|
||||||
|
std::wcout << ModifiersToString(modifiers) << L"+" << VKeyToString(vkCode);
|
||||||
|
|
||||||
|
if (RegisterHotKey(nullptr, hotkeyId, modifiers, vkCode))
|
||||||
|
{
|
||||||
|
m_hotkeyExRegistered = true;
|
||||||
|
m_hotkeyExId = hotkeyId;
|
||||||
|
|
||||||
|
std::wcout << L" - OK (Activation/Toggle)\n";
|
||||||
|
anyRegistered = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DWORD error = GetLastError();
|
||||||
|
std::wcout << L" - FAILED (Error: " << error << L")\n";
|
||||||
|
|
||||||
|
if (error == ERROR_HOTKEY_ALREADY_REGISTERED)
|
||||||
|
{
|
||||||
|
std::wcout << L" (Hotkey is already registered by another application)\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check the legacy get_hotkeys() API
|
||||||
|
size_t hotkeyCount = moduleLoader.GetHotkeys(nullptr, 0);
|
||||||
|
if (hotkeyCount > 0)
|
||||||
|
{
|
||||||
|
std::wcout << L"Module reports " << hotkeyCount << L" legacy hotkey(s)\n";
|
||||||
|
|
||||||
|
// Allocate buffer and get the hotkeys
|
||||||
|
std::vector<PowertoyModuleIface::Hotkey> hotkeys(hotkeyCount);
|
||||||
|
size_t actualCount = moduleLoader.GetHotkeys(hotkeys.data(), hotkeyCount);
|
||||||
|
|
||||||
|
// Register each hotkey
|
||||||
|
for (size_t i = 0; i < actualCount; i++)
|
||||||
|
{
|
||||||
|
const auto& hotkey = hotkeys[i];
|
||||||
|
|
||||||
|
UINT modifiers = ConvertModifiers(hotkey.win, hotkey.ctrl, hotkey.alt, hotkey.shift);
|
||||||
|
UINT vkCode = hotkey.key;
|
||||||
|
|
||||||
|
if (vkCode == 0)
|
||||||
|
{
|
||||||
|
std::wcout << L" Skipping hotkey " << i << L" (no key code)\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int hotkeyId = m_nextHotkeyId++;
|
||||||
|
|
||||||
|
std::wcout << L" Registering hotkey " << i << L": ";
|
||||||
|
std::wcout << ModifiersToString(modifiers) << L"+" << VKeyToString(vkCode);
|
||||||
|
|
||||||
|
if (RegisterHotKey(nullptr, hotkeyId, modifiers, vkCode))
|
||||||
|
{
|
||||||
|
HotkeyInfo info;
|
||||||
|
info.id = hotkeyId;
|
||||||
|
info.moduleHotkeyId = i;
|
||||||
|
info.modifiers = modifiers;
|
||||||
|
info.vkCode = vkCode;
|
||||||
|
info.description = ModifiersToString(modifiers) + L"+" + VKeyToString(vkCode);
|
||||||
|
|
||||||
|
m_registeredHotkeys.push_back(info);
|
||||||
|
std::wcout << L" - OK\n";
|
||||||
|
anyRegistered = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DWORD error = GetLastError();
|
||||||
|
std::wcout << L" - FAILED (Error: " << error << L")\n";
|
||||||
|
|
||||||
|
if (error == ERROR_HOTKEY_ALREADY_REGISTERED)
|
||||||
|
{
|
||||||
|
std::wcout << L" (Hotkey is already registered by another application)\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!anyRegistered && hotkeyCount == 0 && !hotkeyEx.has_value())
|
||||||
|
{
|
||||||
|
std::wcout << L"Module has no hotkeys\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return anyRegistered;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HotkeyManager::UnregisterAll()
|
||||||
|
{
|
||||||
|
for (const auto& hotkey : m_registeredHotkeys)
|
||||||
|
{
|
||||||
|
UnregisterHotKey(nullptr, hotkey.id);
|
||||||
|
}
|
||||||
|
m_registeredHotkeys.clear();
|
||||||
|
|
||||||
|
if (m_hotkeyExRegistered)
|
||||||
|
{
|
||||||
|
UnregisterHotKey(nullptr, m_hotkeyExId);
|
||||||
|
m_hotkeyExRegistered = false;
|
||||||
|
m_hotkeyExId = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HotkeyManager::HandleHotkey(int hotkeyId, ModuleLoader& moduleLoader)
|
||||||
|
{
|
||||||
|
// Check if it's the HotkeyEx activation hotkey
|
||||||
|
if (m_hotkeyExRegistered && hotkeyId == m_hotkeyExId)
|
||||||
|
{
|
||||||
|
std::wcout << L"\nActivation hotkey triggered (HotkeyEx)\n";
|
||||||
|
|
||||||
|
moduleLoader.OnHotkeyEx();
|
||||||
|
|
||||||
|
std::wcout << L"Module toggled via activation hotkey\n";
|
||||||
|
std::wcout << L"Module enabled: " << (moduleLoader.IsEnabled() ? L"Yes" : L"No") << L"\n\n";
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check legacy hotkeys
|
||||||
|
for (const auto& hotkey : m_registeredHotkeys)
|
||||||
|
{
|
||||||
|
if (hotkey.id == hotkeyId)
|
||||||
|
{
|
||||||
|
std::wcout << L"\nHotkey triggered: " << hotkey.description << L"\n";
|
||||||
|
|
||||||
|
bool result = moduleLoader.OnHotkey(hotkey.moduleHotkeyId);
|
||||||
|
|
||||||
|
std::wcout << L"Module handled hotkey: " << (result ? L"Swallowed" : L"Not swallowed") << L"\n";
|
||||||
|
std::wcout << L"Module enabled: " << (moduleLoader.IsEnabled() ? L"Yes" : L"No") << L"\n\n";
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HotkeyManager::PrintHotkeys() const
|
||||||
|
{
|
||||||
|
for (const auto& hotkey : m_registeredHotkeys)
|
||||||
|
{
|
||||||
|
std::wcout << L" " << hotkey.description << L"\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring HotkeyManager::ModifiersToString(UINT modifiers) const
|
||||||
|
{
|
||||||
|
std::wstringstream ss;
|
||||||
|
bool first = true;
|
||||||
|
|
||||||
|
if (modifiers & MOD_WIN)
|
||||||
|
{
|
||||||
|
if (!first) ss << L"+";
|
||||||
|
ss << L"Win";
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
if (modifiers & MOD_CONTROL)
|
||||||
|
{
|
||||||
|
if (!first) ss << L"+";
|
||||||
|
ss << L"Ctrl";
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
if (modifiers & MOD_ALT)
|
||||||
|
{
|
||||||
|
if (!first) ss << L"+";
|
||||||
|
ss << L"Alt";
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
if (modifiers & MOD_SHIFT)
|
||||||
|
{
|
||||||
|
if (!first) ss << L"+";
|
||||||
|
ss << L"Shift";
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring HotkeyManager::VKeyToString(UINT vkCode) const
|
||||||
|
{
|
||||||
|
// Handle special keys
|
||||||
|
switch (vkCode)
|
||||||
|
{
|
||||||
|
case VK_SPACE: return L"Space";
|
||||||
|
case VK_RETURN: return L"Enter";
|
||||||
|
case VK_ESCAPE: return L"Esc";
|
||||||
|
case VK_TAB: return L"Tab";
|
||||||
|
case VK_BACK: return L"Backspace";
|
||||||
|
case VK_DELETE: return L"Del";
|
||||||
|
case VK_INSERT: return L"Ins";
|
||||||
|
case VK_HOME: return L"Home";
|
||||||
|
case VK_END: return L"End";
|
||||||
|
case VK_PRIOR: return L"PgUp";
|
||||||
|
case VK_NEXT: return L"PgDn";
|
||||||
|
case VK_LEFT: return L"Left";
|
||||||
|
case VK_RIGHT: return L"Right";
|
||||||
|
case VK_UP: return L"Up";
|
||||||
|
case VK_DOWN: return L"Down";
|
||||||
|
case VK_F1: return L"F1";
|
||||||
|
case VK_F2: return L"F2";
|
||||||
|
case VK_F3: return L"F3";
|
||||||
|
case VK_F4: return L"F4";
|
||||||
|
case VK_F5: return L"F5";
|
||||||
|
case VK_F6: return L"F6";
|
||||||
|
case VK_F7: return L"F7";
|
||||||
|
case VK_F8: return L"F8";
|
||||||
|
case VK_F9: return L"F9";
|
||||||
|
case VK_F10: return L"F10";
|
||||||
|
case VK_F11: return L"F11";
|
||||||
|
case VK_F12: return L"F12";
|
||||||
|
}
|
||||||
|
|
||||||
|
// For alphanumeric keys, use MapVirtualKey
|
||||||
|
wchar_t keyName[256];
|
||||||
|
UINT scanCode = MapVirtualKeyW(vkCode, MAPVK_VK_TO_VSC);
|
||||||
|
|
||||||
|
if (GetKeyNameTextW(scanCode << 16, keyName, 256) > 0)
|
||||||
|
{
|
||||||
|
return keyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to hex code
|
||||||
|
std::wstringstream ss;
|
||||||
|
ss << L"0x" << std::hex << vkCode;
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
86
tools/module_loader/src/HotkeyManager.h
Normal file
86
tools/module_loader/src/HotkeyManager.h
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Windows.h>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <map>
|
||||||
|
#include "ModuleLoader.h"
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manages hotkey registration using RegisterHotKey API
|
||||||
|
/// </summary>
|
||||||
|
class HotkeyManager
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
HotkeyManager();
|
||||||
|
~HotkeyManager();
|
||||||
|
|
||||||
|
// Prevent copying
|
||||||
|
HotkeyManager(const HotkeyManager&) = delete;
|
||||||
|
HotkeyManager& operator=(const HotkeyManager&) = delete;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register all hotkeys from a module
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="moduleLoader">Module to get hotkeys from</param>
|
||||||
|
/// <returns>True if at least one hotkey was registered</returns>
|
||||||
|
bool RegisterModuleHotkeys(ModuleLoader& moduleLoader);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unregister all hotkeys
|
||||||
|
/// </summary>
|
||||||
|
void UnregisterAll();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handle a WM_HOTKEY message
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hotkeyId">ID from the WM_HOTKEY message</param>
|
||||||
|
/// <param name="moduleLoader">Module to trigger the hotkey on</param>
|
||||||
|
/// <returns>True if the hotkey was handled</returns>
|
||||||
|
bool HandleHotkey(int hotkeyId, ModuleLoader& moduleLoader);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the number of registered hotkeys
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Number of registered hotkeys</returns>
|
||||||
|
size_t GetRegisteredCount() const { return m_registeredHotkeys.size() + (m_hotkeyExRegistered ? 1 : 0); }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Print registered hotkeys to console
|
||||||
|
/// </summary>
|
||||||
|
void PrintHotkeys() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct HotkeyInfo
|
||||||
|
{
|
||||||
|
int id = 0;
|
||||||
|
size_t moduleHotkeyId = 0;
|
||||||
|
UINT modifiers = 0;
|
||||||
|
UINT vkCode = 0;
|
||||||
|
std::wstring description;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<HotkeyInfo> m_registeredHotkeys;
|
||||||
|
int m_nextHotkeyId;
|
||||||
|
bool m_hotkeyExRegistered;
|
||||||
|
int m_hotkeyExId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert modifier bools to RegisterHotKey modifiers
|
||||||
|
/// </summary>
|
||||||
|
UINT ConvertModifiers(bool win, bool ctrl, bool alt, bool shift) const;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a string representation of modifiers
|
||||||
|
/// </summary>
|
||||||
|
std::wstring ModifiersToString(UINT modifiers) const;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a string representation of a virtual key code
|
||||||
|
/// </summary>
|
||||||
|
std::wstring VKeyToString(UINT vkCode) const;
|
||||||
|
};
|
||||||
183
tools/module_loader/src/ModuleLoader.cpp
Normal file
183
tools/module_loader/src/ModuleLoader.cpp
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
#include "ModuleLoader.h"
|
||||||
|
#include <iostream>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
ModuleLoader::ModuleLoader()
|
||||||
|
: m_hModule(nullptr)
|
||||||
|
, m_module(nullptr)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
ModuleLoader::~ModuleLoader()
|
||||||
|
{
|
||||||
|
if (m_module)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
m_module->destroy();
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
// Ignore exceptions during cleanup
|
||||||
|
}
|
||||||
|
m_module = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_hModule)
|
||||||
|
{
|
||||||
|
FreeLibrary(m_hModule);
|
||||||
|
m_hModule = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ModuleLoader::Load(const std::wstring& dllPath)
|
||||||
|
{
|
||||||
|
if (m_hModule || m_module)
|
||||||
|
{
|
||||||
|
std::wcerr << L"Error: Module already loaded\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_dllPath = dllPath;
|
||||||
|
|
||||||
|
// Load the DLL
|
||||||
|
m_hModule = LoadLibraryW(dllPath.c_str());
|
||||||
|
if (!m_hModule)
|
||||||
|
{
|
||||||
|
DWORD error = GetLastError();
|
||||||
|
std::wcerr << L"Error: Failed to load DLL. Error code: " << error << L"\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the powertoy_create function
|
||||||
|
using powertoy_create_func = PowertoyModuleIface* (*)();
|
||||||
|
auto create_func = reinterpret_cast<powertoy_create_func>(
|
||||||
|
GetProcAddress(m_hModule, "powertoy_create"));
|
||||||
|
|
||||||
|
if (!create_func)
|
||||||
|
{
|
||||||
|
std::wcerr << L"Error: DLL does not export 'powertoy_create' function\n";
|
||||||
|
FreeLibrary(m_hModule);
|
||||||
|
m_hModule = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the module instance
|
||||||
|
m_module = create_func();
|
||||||
|
if (!m_module)
|
||||||
|
{
|
||||||
|
std::wcerr << L"Error: powertoy_create() returned nullptr\n";
|
||||||
|
FreeLibrary(m_hModule);
|
||||||
|
m_hModule = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wcout << L"Module instance created successfully\n";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ModuleLoader::Enable()
|
||||||
|
{
|
||||||
|
if (!m_module)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Module not loaded");
|
||||||
|
}
|
||||||
|
|
||||||
|
m_module->enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ModuleLoader::Disable()
|
||||||
|
{
|
||||||
|
if (!m_module)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_module->disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ModuleLoader::IsEnabled() const
|
||||||
|
{
|
||||||
|
if (!m_module)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return m_module->is_enabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ModuleLoader::SetConfig(const std::wstring& configJson)
|
||||||
|
{
|
||||||
|
if (!m_module)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Module not loaded");
|
||||||
|
}
|
||||||
|
|
||||||
|
m_module->set_config(configJson.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring ModuleLoader::GetModuleName() const
|
||||||
|
{
|
||||||
|
if (!m_module)
|
||||||
|
{
|
||||||
|
return L"<not loaded>";
|
||||||
|
}
|
||||||
|
|
||||||
|
const wchar_t* name = m_module->get_name();
|
||||||
|
return name ? name : L"<unknown>";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring ModuleLoader::GetModuleKey() const
|
||||||
|
{
|
||||||
|
if (!m_module)
|
||||||
|
{
|
||||||
|
return L"<not loaded>";
|
||||||
|
}
|
||||||
|
|
||||||
|
const wchar_t* key = m_module->get_key();
|
||||||
|
return key ? key : L"<unknown>";
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t ModuleLoader::GetHotkeys(PowertoyModuleIface::Hotkey* buffer, size_t bufferSize)
|
||||||
|
{
|
||||||
|
if (!m_module)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return m_module->get_hotkeys(buffer, bufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ModuleLoader::OnHotkey(size_t hotkeyId)
|
||||||
|
{
|
||||||
|
if (!m_module)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return m_module->on_hotkey(hotkeyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<PowertoyModuleIface::HotkeyEx> ModuleLoader::GetHotkeyEx()
|
||||||
|
{
|
||||||
|
if (!m_module)
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return m_module->GetHotkeyEx();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ModuleLoader::OnHotkeyEx()
|
||||||
|
{
|
||||||
|
if (!m_module)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_module->OnHotkeyEx();
|
||||||
|
}
|
||||||
102
tools/module_loader/src/ModuleLoader.h
Normal file
102
tools/module_loader/src/ModuleLoader.h
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Windows.h>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <powertoy_module_interface.h>
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wrapper class for loading and managing a PowerToy module DLL
|
||||||
|
/// </summary>
|
||||||
|
class ModuleLoader
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ModuleLoader();
|
||||||
|
~ModuleLoader();
|
||||||
|
|
||||||
|
// Prevent copying
|
||||||
|
ModuleLoader(const ModuleLoader&) = delete;
|
||||||
|
ModuleLoader& operator=(const ModuleLoader&) = delete;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Load a PowerToy module DLL
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dllPath">Path to the module DLL</param>
|
||||||
|
/// <returns>True if successful, false otherwise</returns>
|
||||||
|
bool Load(const std::wstring& dllPath);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enable the loaded module
|
||||||
|
/// </summary>
|
||||||
|
void Enable();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disable the loaded module
|
||||||
|
/// </summary>
|
||||||
|
void Disable();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if the module is enabled
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True if enabled, false otherwise</returns>
|
||||||
|
bool IsEnabled() const;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set configuration for the module
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configJson">JSON configuration string</param>
|
||||||
|
void SetConfig(const std::wstring& configJson);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the module's localized name
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Module name</returns>
|
||||||
|
std::wstring GetModuleName() const;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the module's non-localized key
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Module key</returns>
|
||||||
|
std::wstring GetModuleKey() const;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the module's hotkeys
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="buffer">Buffer to store hotkeys</param>
|
||||||
|
/// <param name="bufferSize">Size of the buffer</param>
|
||||||
|
/// <returns>Number of hotkeys returned</returns>
|
||||||
|
size_t GetHotkeys(PowertoyModuleIface::Hotkey* buffer, size_t bufferSize);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger a hotkey callback on the module
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hotkeyId">ID of the hotkey to trigger</param>
|
||||||
|
/// <returns>True if the key press should be swallowed</returns>
|
||||||
|
bool OnHotkey(size_t hotkeyId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if the module is loaded
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True if loaded, false otherwise</returns>
|
||||||
|
bool IsLoaded() const { return m_module != nullptr; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the module's activation hotkey (newer HotkeyEx API)
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Optional HotkeyEx struct</returns>
|
||||||
|
std::optional<PowertoyModuleIface::HotkeyEx> GetHotkeyEx();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger the newer-style hotkey callback on the module
|
||||||
|
/// </summary>
|
||||||
|
void OnHotkeyEx();
|
||||||
|
|
||||||
|
private:
|
||||||
|
HMODULE m_hModule;
|
||||||
|
PowertoyModuleIface* m_module;
|
||||||
|
std::wstring m_dllPath;
|
||||||
|
};
|
||||||
182
tools/module_loader/src/SettingsLoader.cpp
Normal file
182
tools/module_loader/src/SettingsLoader.cpp
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
#include "SettingsLoader.h"
|
||||||
|
#include <iostream>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <Shlobj.h>
|
||||||
|
|
||||||
|
SettingsLoader::SettingsLoader()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsLoader::~SettingsLoader()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring SettingsLoader::GetPowerToysSettingsRoot() const
|
||||||
|
{
|
||||||
|
// Get %LOCALAPPDATA%
|
||||||
|
PWSTR localAppDataPath = nullptr;
|
||||||
|
HRESULT hr = SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &localAppDataPath);
|
||||||
|
|
||||||
|
if (FAILED(hr) || !localAppDataPath)
|
||||||
|
{
|
||||||
|
std::wcerr << L"Error: Failed to get LOCALAPPDATA path\n";
|
||||||
|
return L"";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring result(localAppDataPath);
|
||||||
|
CoTaskMemFree(localAppDataPath);
|
||||||
|
|
||||||
|
// Append PowerToys directory
|
||||||
|
result += L"\\Microsoft\\PowerToys";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring SettingsLoader::GetSettingsPath(const std::wstring& moduleName) const
|
||||||
|
{
|
||||||
|
std::wstring root = GetPowerToysSettingsRoot();
|
||||||
|
if (root.empty())
|
||||||
|
{
|
||||||
|
return L"";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct path: %LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\settings.json
|
||||||
|
std::wstring settingsPath = root + L"\\" + moduleName + L"\\settings.json";
|
||||||
|
return settingsPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring SettingsLoader::ReadFileContents(const std::wstring& filePath) const
|
||||||
|
{
|
||||||
|
std::wifstream file(filePath, std::ios::binary);
|
||||||
|
if (!file.is_open())
|
||||||
|
{
|
||||||
|
std::wcerr << L"Error: Could not open file: " << filePath << L"\n";
|
||||||
|
return L"";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the entire file
|
||||||
|
std::wstringstream buffer;
|
||||||
|
buffer << file.rdbuf();
|
||||||
|
|
||||||
|
return buffer.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring SettingsLoader::LoadSettings(const std::wstring& moduleName, const std::wstring& moduleDllPath)
|
||||||
|
{
|
||||||
|
const std::wstring powerToysPrefix = L"PowerToys.";
|
||||||
|
|
||||||
|
// Build list of possible module name variations to try
|
||||||
|
std::vector<std::wstring> moduleNameVariants;
|
||||||
|
|
||||||
|
// Try exact name first
|
||||||
|
moduleNameVariants.push_back(moduleName);
|
||||||
|
|
||||||
|
// If doesn't start with "PowerToys.", try adding it
|
||||||
|
if (moduleName.find(powerToysPrefix) != 0)
|
||||||
|
{
|
||||||
|
moduleNameVariants.push_back(powerToysPrefix + moduleName);
|
||||||
|
}
|
||||||
|
// If starts with "PowerToys.", try without it
|
||||||
|
else
|
||||||
|
{
|
||||||
|
moduleNameVariants.push_back(moduleName.substr(powerToysPrefix.length()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIRST: Try same directory as the module DLL
|
||||||
|
if (!moduleDllPath.empty())
|
||||||
|
{
|
||||||
|
std::filesystem::path dllPath(moduleDllPath);
|
||||||
|
std::filesystem::path dllDirectory = dllPath.parent_path();
|
||||||
|
|
||||||
|
std::wstring localSettingsPath = (dllDirectory / L"settings.json").wstring();
|
||||||
|
std::wcout << L"Trying settings path (module directory): " << localSettingsPath << L"\n";
|
||||||
|
|
||||||
|
if (std::filesystem::exists(localSettingsPath))
|
||||||
|
{
|
||||||
|
std::wstring contents = ReadFileContents(localSettingsPath);
|
||||||
|
if (!contents.empty())
|
||||||
|
{
|
||||||
|
std::wcout << L"Settings file loaded from module directory (" << contents.size() << L" characters)\n";
|
||||||
|
return contents;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SECOND: Try standard PowerToys settings locations
|
||||||
|
for (const auto& variant : moduleNameVariants)
|
||||||
|
{
|
||||||
|
std::wstring settingsPath = GetSettingsPath(variant);
|
||||||
|
|
||||||
|
std::wcout << L"Trying settings path: " << settingsPath << L"\n";
|
||||||
|
|
||||||
|
// Check if file exists (case-sensitive path)
|
||||||
|
if (std::filesystem::exists(settingsPath))
|
||||||
|
{
|
||||||
|
std::wstring contents = ReadFileContents(settingsPath);
|
||||||
|
if (!contents.empty())
|
||||||
|
{
|
||||||
|
std::wcout << L"Settings file loaded (" << contents.size() << L" characters)\n";
|
||||||
|
return contents;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Try case-insensitive search in the parent directory
|
||||||
|
std::wstring root = GetPowerToysSettingsRoot();
|
||||||
|
if (!root.empty() && std::filesystem::exists(root))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Search for a directory that matches case-insensitively
|
||||||
|
for (const auto& entry : std::filesystem::directory_iterator(root))
|
||||||
|
{
|
||||||
|
if (entry.is_directory())
|
||||||
|
{
|
||||||
|
std::wstring dirName = entry.path().filename().wstring();
|
||||||
|
|
||||||
|
// Case-insensitive comparison
|
||||||
|
if (_wcsicmp(dirName.c_str(), variant.c_str()) == 0)
|
||||||
|
{
|
||||||
|
std::wstring actualSettingsPath = entry.path().wstring() + L"\\settings.json";
|
||||||
|
std::wcout << L"Found case-insensitive match: " << actualSettingsPath << L"\n";
|
||||||
|
|
||||||
|
if (std::filesystem::exists(actualSettingsPath))
|
||||||
|
{
|
||||||
|
std::wstring contents = ReadFileContents(actualSettingsPath);
|
||||||
|
if (!contents.empty())
|
||||||
|
{
|
||||||
|
std::wcout << L"Settings file loaded (" << contents.size() << L" characters)\n";
|
||||||
|
return contents;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (const std::filesystem::filesystem_error& e)
|
||||||
|
{
|
||||||
|
std::wcerr << L"Error searching directory: " << e.what() << L"\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wcerr << L"Error: Settings file not found in any expected location:\n";
|
||||||
|
if (!moduleDllPath.empty())
|
||||||
|
{
|
||||||
|
std::filesystem::path dllPath(moduleDllPath);
|
||||||
|
std::filesystem::path dllDirectory = dllPath.parent_path();
|
||||||
|
std::wcerr << L" - " << (dllDirectory / L"settings.json").wstring() << L" (module directory)\n";
|
||||||
|
}
|
||||||
|
for (const auto& variant : moduleNameVariants)
|
||||||
|
{
|
||||||
|
std::wcerr << L" - " << GetSettingsPath(variant) << L"\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return L"";
|
||||||
|
}
|
||||||
47
tools/module_loader/src/SettingsLoader.h
Normal file
47
tools/module_loader/src/SettingsLoader.h
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Windows.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Utility class for discovering and loading PowerToy module settings
|
||||||
|
/// </summary>
|
||||||
|
class SettingsLoader
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SettingsLoader();
|
||||||
|
~SettingsLoader();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Load settings for a PowerToy module
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="moduleName">Name of the module (e.g., "CursorWrap")</param>
|
||||||
|
/// <param name="moduleDllPath">Full path to the module DLL (for checking local settings.json)</param>
|
||||||
|
/// <returns>JSON settings string, or empty string if not found</returns>
|
||||||
|
std::wstring LoadSettings(const std::wstring& moduleName, const std::wstring& moduleDllPath);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the settings file path for a module
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="moduleName">Name of the module</param>
|
||||||
|
/// <returns>Full path to the settings.json file</returns>
|
||||||
|
std::wstring GetSettingsPath(const std::wstring& moduleName) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
/// <summary>
|
||||||
|
/// Get the PowerToys root settings directory
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Path to %LOCALAPPDATA%\Microsoft\PowerToys</returns>
|
||||||
|
std::wstring GetPowerToysSettingsRoot() const;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read a text file into a string
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filePath">Path to the file</param>
|
||||||
|
/// <returns>File contents as a string</returns>
|
||||||
|
std::wstring ReadFileContents(const std::wstring& filePath) const;
|
||||||
|
};
|
||||||
244
tools/module_loader/src/main.cpp
Normal file
244
tools/module_loader/src/main.cpp
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
#include <Windows.h>
|
||||||
|
#include <Tlhelp32.h>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <filesystem>
|
||||||
|
#include "ModuleLoader.h"
|
||||||
|
#include "SettingsLoader.h"
|
||||||
|
#include "HotkeyManager.h"
|
||||||
|
#include "ConsoleHost.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
void PrintUsage()
|
||||||
|
{
|
||||||
|
std::wcout << L"PowerToys Module Loader - Standalone utility for loading and testing PowerToy modules\n\n";
|
||||||
|
std::wcout << L"Usage: ModuleLoader.exe <module_dll_path>\n\n";
|
||||||
|
std::wcout << L"Arguments:\n";
|
||||||
|
std::wcout << L" module_dll_path Path to the PowerToy module DLL (e.g., CursorWrap.dll)\n\n";
|
||||||
|
std::wcout << L"Behavior:\n";
|
||||||
|
std::wcout << L" - Automatically discovers settings from %%LOCALAPPDATA%%\\Microsoft\\PowerToys\\<ModuleName>\\settings.json\n";
|
||||||
|
std::wcout << L" - Loads and enables the module\n";
|
||||||
|
std::wcout << L" - Registers module hotkeys\n";
|
||||||
|
std::wcout << L" - Runs until Ctrl+C is pressed\n\n";
|
||||||
|
std::wcout << L"Examples:\n";
|
||||||
|
std::wcout << L" ModuleLoader.exe x64\\Debug\\modules\\CursorWrap.dll\n";
|
||||||
|
std::wcout << L" ModuleLoader.exe \"C:\\Program Files\\PowerToys\\modules\\MouseHighlighter.dll\"\n\n";
|
||||||
|
std::wcout << L"Notes:\n";
|
||||||
|
std::wcout << L" - Only non-UI modules are supported\n";
|
||||||
|
std::wcout << L" - Module must have a valid settings.json file\n";
|
||||||
|
std::wcout << L" - Debug output is written to module's log directory\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring ExtractModuleName(const std::wstring& dllPath)
|
||||||
|
{
|
||||||
|
std::filesystem::path path(dllPath);
|
||||||
|
std::wstring filename = path.stem().wstring();
|
||||||
|
|
||||||
|
// Remove "PowerToys." prefix if present (case-insensitive)
|
||||||
|
const std::wstring powerToysPrefix = L"PowerToys.";
|
||||||
|
if (filename.length() >= powerToysPrefix.length())
|
||||||
|
{
|
||||||
|
// Check if filename starts with "PowerToys." (case-insensitive)
|
||||||
|
if (_wcsnicmp(filename.c_str(), powerToysPrefix.c_str(), powerToysPrefix.length()) == 0)
|
||||||
|
{
|
||||||
|
filename = filename.substr(powerToysPrefix.length());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common PowerToys module naming patterns
|
||||||
|
// Remove common suffixes if present
|
||||||
|
const std::wstring suffixes[] = { L"Module", L"ModuleInterface", L"Interface" };
|
||||||
|
for (const auto& suffix : suffixes)
|
||||||
|
{
|
||||||
|
if (filename.size() > suffix.size())
|
||||||
|
{
|
||||||
|
size_t pos = filename.rfind(suffix);
|
||||||
|
if (pos != std::wstring::npos && pos + suffix.size() == filename.size())
|
||||||
|
{
|
||||||
|
filename = filename.substr(0, pos);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int wmain(int argc, wchar_t* argv[])
|
||||||
|
{
|
||||||
|
std::wcout << L"PowerToys Module Loader v1.0\n";
|
||||||
|
std::wcout << L"=============================\n\n";
|
||||||
|
|
||||||
|
// Check if PowerToys.exe is running
|
||||||
|
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||||
|
if (hSnapshot != INVALID_HANDLE_VALUE)
|
||||||
|
{
|
||||||
|
PROCESSENTRY32W pe32;
|
||||||
|
pe32.dwSize = sizeof(PROCESSENTRY32W);
|
||||||
|
|
||||||
|
bool powerToysRunning = false;
|
||||||
|
if (Process32FirstW(hSnapshot, &pe32))
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if (_wcsicmp(pe32.szExeFile, L"PowerToys.exe") == 0)
|
||||||
|
{
|
||||||
|
powerToysRunning = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while (Process32NextW(hSnapshot, &pe32));
|
||||||
|
}
|
||||||
|
CloseHandle(hSnapshot);
|
||||||
|
|
||||||
|
if (powerToysRunning)
|
||||||
|
{
|
||||||
|
// Display warning with VT100 colors
|
||||||
|
// Yellow background (43m), black text (30m), bold (1m)
|
||||||
|
std::wcout << L"\033[1;43;30m WARNING \033[0m PowerToys.exe is currently running!\n\n";
|
||||||
|
|
||||||
|
// Red text for important message
|
||||||
|
std::wcout << L"\033[1;31m";
|
||||||
|
std::wcout << L"Running ModuleLoader while PowerToys is active may cause conflicts:\n";
|
||||||
|
std::wcout << L" - Duplicate hotkey registrations\n";
|
||||||
|
std::wcout << L" - Conflicting module instances\n";
|
||||||
|
std::wcout << L" - Unexpected behavior\n";
|
||||||
|
std::wcout << L"\033[0m\n"; // Reset color
|
||||||
|
|
||||||
|
// Cyan text for recommendation
|
||||||
|
std::wcout << L"\033[1;36m";
|
||||||
|
std::wcout << L"RECOMMENDATION: Exit PowerToys before continuing.\n";
|
||||||
|
std::wcout << L"\033[0m\n"; // Reset color
|
||||||
|
|
||||||
|
// Yellow text for prompt
|
||||||
|
std::wcout << L"\033[1;33m";
|
||||||
|
std::wcout << L"Do you want to continue anyway? (y/N): ";
|
||||||
|
std::wcout << L"\033[0m"; // Reset color
|
||||||
|
|
||||||
|
wchar_t response = L'\0';
|
||||||
|
std::wcin >> response;
|
||||||
|
|
||||||
|
if (response != L'y' && response != L'Y')
|
||||||
|
{
|
||||||
|
std::wcout << L"\nExiting. Please close PowerToys and try again.\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wcout << L"\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse command-line arguments
|
||||||
|
if (argc < 2)
|
||||||
|
{
|
||||||
|
std::wcerr << L"Error: Missing required argument <module_dll_path>\n\n";
|
||||||
|
PrintUsage();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::wstring dllPath = argv[1];
|
||||||
|
|
||||||
|
// Validate DLL exists
|
||||||
|
if (!std::filesystem::exists(dllPath))
|
||||||
|
{
|
||||||
|
std::wcerr << L"Error: Module DLL not found: " << dllPath << L"\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wcout << L"Loading module: " << dllPath << L"\n";
|
||||||
|
|
||||||
|
// Extract module name from DLL path
|
||||||
|
std::wstring moduleName = ExtractModuleName(dllPath);
|
||||||
|
std::wcout << L"Detected module name: " << moduleName << L"\n\n";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Load settings for the module
|
||||||
|
std::wcout << L"Loading settings...\n";
|
||||||
|
SettingsLoader settingsLoader;
|
||||||
|
std::wstring settingsJson = settingsLoader.LoadSettings(moduleName, dllPath);
|
||||||
|
|
||||||
|
if (settingsJson.empty())
|
||||||
|
{
|
||||||
|
std::wcerr << L"Error: Could not load settings for module '" << moduleName << L"'\n";
|
||||||
|
std::wcerr << L"Expected location: %LOCALAPPDATA%\\Microsoft\\PowerToys\\" << moduleName << L"\\settings.json\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wcout << L"Settings loaded successfully.\n\n";
|
||||||
|
|
||||||
|
// Load the module DLL
|
||||||
|
std::wcout << L"Loading module DLL...\n";
|
||||||
|
ModuleLoader moduleLoader;
|
||||||
|
if (!moduleLoader.Load(dllPath))
|
||||||
|
{
|
||||||
|
std::wcerr << L"Error: Failed to load module DLL\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wcout << L"Module DLL loaded successfully.\n";
|
||||||
|
std::wcout << L"Module key: " << moduleLoader.GetModuleKey() << L"\n";
|
||||||
|
std::wcout << L"Module name: " << moduleLoader.GetModuleName() << L"\n\n";
|
||||||
|
|
||||||
|
// Apply settings to the module
|
||||||
|
std::wcout << L"Applying settings to module...\n";
|
||||||
|
moduleLoader.SetConfig(settingsJson);
|
||||||
|
std::wcout << L"Settings applied.\n\n";
|
||||||
|
|
||||||
|
// Register hotkeys
|
||||||
|
std::wcout << L"Registering module hotkeys...\n";
|
||||||
|
HotkeyManager hotkeyManager;
|
||||||
|
if (!hotkeyManager.RegisterModuleHotkeys(moduleLoader))
|
||||||
|
{
|
||||||
|
std::wcerr << L"Warning: Failed to register some hotkeys\n";
|
||||||
|
}
|
||||||
|
std::wcout << L"Hotkeys registered: " << hotkeyManager.GetRegisteredCount() << L"\n\n";
|
||||||
|
|
||||||
|
// Enable the module
|
||||||
|
std::wcout << L"Enabling module...\n";
|
||||||
|
moduleLoader.Enable();
|
||||||
|
std::wcout << L"Module enabled.\n\n";
|
||||||
|
|
||||||
|
// Display status
|
||||||
|
std::wcout << L"=============================\n";
|
||||||
|
std::wcout << L"Module is now running!\n";
|
||||||
|
std::wcout << L"=============================\n\n";
|
||||||
|
std::wcout << L"Module Status:\n";
|
||||||
|
std::wcout << L" - Name: " << moduleLoader.GetModuleName() << L"\n";
|
||||||
|
std::wcout << L" - Key: " << moduleLoader.GetModuleKey() << L"\n";
|
||||||
|
std::wcout << L" - Enabled: " << (moduleLoader.IsEnabled() ? L"Yes" : L"No") << L"\n";
|
||||||
|
std::wcout << L" - Hotkeys: " << hotkeyManager.GetRegisteredCount() << L" registered\n\n";
|
||||||
|
|
||||||
|
if (hotkeyManager.GetRegisteredCount() > 0)
|
||||||
|
{
|
||||||
|
std::wcout << L"Registered Hotkeys:\n";
|
||||||
|
hotkeyManager.PrintHotkeys();
|
||||||
|
std::wcout << L"\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wcout << L"Press Ctrl+C to exit.\n";
|
||||||
|
std::wcout << L"You can press the module's hotkey to toggle its functionality.\n\n";
|
||||||
|
|
||||||
|
// Run the message loop
|
||||||
|
ConsoleHost consoleHost(moduleLoader, hotkeyManager);
|
||||||
|
consoleHost.Run();
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
std::wcout << L"\nShutting down...\n";
|
||||||
|
moduleLoader.Disable();
|
||||||
|
hotkeyManager.UnregisterAll();
|
||||||
|
|
||||||
|
std::wcout << L"Module unloaded successfully.\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
catch (const std::exception& ex)
|
||||||
|
{
|
||||||
|
std::wcerr << L"Fatal error: " << ex.what() << L"\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user