Compare commits
21 Commits
dev/crutka
...
copilot/ch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97f2868481 | ||
|
|
f3d3abc552 | ||
|
|
0a64561ed5 | ||
|
|
7f19817182 | ||
|
|
a33fd3c474 | ||
|
|
b712fa4d85 | ||
|
|
b66b044210 | ||
|
|
18919eaa40 | ||
|
|
e0854fbaf3 | ||
|
|
ba20da1611 | ||
|
|
35f2ed839e | ||
|
|
e20b5b9c51 | ||
|
|
c6a9ad2ad0 | ||
|
|
a67fc2d9b7 | ||
|
|
c78f6e52a0 | ||
|
|
9a55209d13 | ||
|
|
0cb6fe250b | ||
|
|
c0cb9417ad | ||
|
|
cd5027fa1a | ||
|
|
7da62cdb0a | ||
|
|
c46083dd8d |
1
.github/actions/spell-check/expect.txt
vendored
@@ -1970,6 +1970,7 @@ UNORM
|
||||
unparsable
|
||||
unremapped
|
||||
Unsend
|
||||
Unsubscribes
|
||||
untriaged
|
||||
unvirtualized
|
||||
unwide
|
||||
|
||||
|
Before Width: | Height: | Size: 275 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 8.4 KiB |
@@ -79,3 +79,4 @@ Below are community created plugins that target a website or software. They are
|
||||
| [Linear](https://github.com/vednig/powertoys-linear) | [vednig](https://github.com/vednig) | Create Linear Issues directly from Powertoys Run |
|
||||
| [PerplexitySearchShortcut](https://github.com/0x6f677548/PowerToys-Run-PerplexitySearchShortcut) | [0x6f677548](https://github.com/0x6f677548) | Search Perplexity |
|
||||
| [SpeedTest](https://github.com/ruslanlap/PowerToysRun-SpeedTest) | [ruslanlap](https://github.com/ruslanlap) | One-command internet speed tests with real-time results, modern UI, and shareable links. |
|
||||
| [DiskAnalyzer](https://github.com/thetsaw/PowerToys.Plugin) | [thetsaw](https://github.com/thetsaw) | Scan folders, find the largest files, and view drive space usage with visual progress bars. |
|
||||
|
||||
@@ -4,15 +4,23 @@
|
||||
|
||||
<?define ShortcutGuideAssetsFiles=?>
|
||||
<?define ShortcutGuideAssetsFilesPath=$(var.BinDir)WinUI3Apps\Assets\ShortcutGuide\?>
|
||||
<?define ShortcutGuideManifestsFiles=?>
|
||||
<?define ShortcutGuideManifestsFilesPath=$(var.BinDir)WinUI3Apps\Assets\ShortcutGuide\Manifests\?>
|
||||
|
||||
<Fragment>
|
||||
<DirectoryRef Id="WinUI3AppsAssetsFolder">
|
||||
<Directory Id="ShortcutGuideAssetsFolder" Name="ShortcutGuide" />
|
||||
<Directory Id="ShortcutGuideAssetsFolder" Name="ShortcutGuide">
|
||||
<Directory Id="ShortcutGuideManifestsFolder" Name="Manifests" />
|
||||
</Directory>
|
||||
</DirectoryRef>
|
||||
<DirectoryRef Id="ShortcutGuideAssetsFolder" FileSource="$(var.ShortcutGuideAssetsFilesPath)">
|
||||
<!-- Generated by generateFileComponents.ps1 -->
|
||||
<!--ShortcutGuideAssetsFiles_Component_Def-->
|
||||
</DirectoryRef>
|
||||
<DirectoryRef Id="ShortcutGuideManifestsFolder" FileSource="$(var.ShortcutGuideManifestsFilesPath)">
|
||||
<!-- Generated by generateFileComponents.ps1 -->
|
||||
<!--ShortcutGuideManifestsFiles_Component_Def-->
|
||||
</DirectoryRef>
|
||||
|
||||
<!-- Shortcut guide -->
|
||||
<ComponentGroup Id="ShortcutGuideComponentGroup" >
|
||||
@@ -22,6 +30,12 @@
|
||||
</RegistryKey>
|
||||
<RemoveFolder Id="RemoveFolderShortcutGuideAssetsInstallFolder" Directory="ShortcutGuideAssetsFolder" On="uninstall"/>
|
||||
</Component>
|
||||
<Component Id="RemoveShortcutGuideManifestsFolder" Guid="F47E2C3A-8D91-4B6F-A2E5-9C8D7F6A1B3E" Directory="ShortcutGuideManifestsFolder" >
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="RemoveShortcutGuideManifestsFolder" Value="" KeyPath="yes"/>
|
||||
</RegistryKey>
|
||||
<RemoveFolder Id="RemoveFolderShortcutGuideManifestsInstallFolder" Directory="ShortcutGuideManifestsFolder" On="uninstall"/>
|
||||
</Component>
|
||||
</ComponentGroup>
|
||||
|
||||
</Fragment>
|
||||
|
||||
@@ -28,7 +28,7 @@ Function Generate-FileList() {
|
||||
|
||||
$fileExclusionList = @("*.pdb", "*.lastcodeanalysissucceeded", "createdump.exe", "powertoys.exe")
|
||||
|
||||
$fileInclusionList = @("*.dll", "*.exe", "*.json", "*.msix", "*.png", "*.gif", "*.ico", "*.cur", "*.svg", "index.html", "reg.js", "gitignore.js", "srt.js", "monacoSpecialLanguages.js", "customTokenThemeRules.js", "*.pri")
|
||||
$fileInclusionList = @("*.dll", "*.exe", "*.json", "*.msix", "*.png", "*.gif", "*.ico", "*.cur", "*.svg", "index.html", "reg.js", "gitignore.js", "srt.js", "monacoSpecialLanguages.js", "customTokenThemeRules.js", "*.pri", "*.yml")
|
||||
|
||||
# MFC DLLs leak into the output via WindowsAppSDKSelfContained but no PowerToys binary imports them.
|
||||
# Verified with dumpbin /dependents across all 2176 binaries — zero consumers.
|
||||
@@ -112,6 +112,8 @@ Function Generate-FileComponents() {
|
||||
|
||||
foreach ($file in $fileList) {
|
||||
$fileTmp = $file -replace "-", "_"
|
||||
$fileTmp = $fileTmp -replace "[^A-Za-z0-9_.]", "_"
|
||||
if ($fileTmp -match "^[^A-Za-z_]") { $fileTmp = "_$fileTmp" }
|
||||
$componentDefs +=
|
||||
@"
|
||||
<File Id="$($fileListName)_File_$($fileTmp)" Source="`$(var.$($fileListName)Path)\$($file)" />`r`n
|
||||
@@ -397,8 +399,24 @@ Generate-FileComponents -fileListName "ValueGeneratorImagesCmpFiles" -wxsFilePat
|
||||
## Plugins
|
||||
|
||||
#ShortcutGuide
|
||||
# Ensure manifest yml files are in the build output (the Build target's CopyToOutputDirectory
|
||||
# may not run reliably under -graph mode in solution builds).
|
||||
$sgManifestsSrc = "$PSScriptRoot..\..\..\src\modules\ShortcutGuide\ShortcutGuide.Ui\Assets\ShortcutGuide\Manifests"
|
||||
$sgManifestsDst = "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\ShortcutGuide\Manifests"
|
||||
Write-Host "ShortcutGuide manifests: src=$sgManifestsSrc exists=$(Test-Path $sgManifestsSrc)"
|
||||
Write-Host "ShortcutGuide manifests: dst=$sgManifestsDst exists=$(Test-Path $sgManifestsDst)"
|
||||
if (Test-Path $sgManifestsSrc) {
|
||||
New-Item -Path $sgManifestsDst -ItemType Directory -Force | Out-Null
|
||||
Copy-Item "$sgManifestsSrc\*.yml" -Destination $sgManifestsDst -Force
|
||||
$copied = (Get-ChildItem "$sgManifestsDst\*.yml" -ErrorAction SilentlyContinue).Count
|
||||
Write-Host "ShortcutGuide manifests: copied $copied yml files to build output"
|
||||
} else {
|
||||
Write-Host "WARNING: ShortcutGuide manifest source not found at $sgManifestsSrc"
|
||||
}
|
||||
Generate-FileList -fileDepsJson "" -fileListName ShortcutGuideAssetsFiles -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\ShortcutGuide\"
|
||||
Generate-FileComponents -fileListName "ShortcutGuideAssetsFiles" -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs -regroot $registryroot
|
||||
Generate-FileComponents -fileListName "ShortcutGuideAssetsFiles" -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs
|
||||
Generate-FileList -fileDepsJson "" -fileListName ShortcutGuideManifestsFiles -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\ShortcutGuide\Manifests\"
|
||||
Generate-FileComponents -fileListName "ShortcutGuideManifestsFiles" -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs
|
||||
|
||||
#Settings
|
||||
Generate-FileList -fileDepsJson "" -fileListName SettingsV2AssetsFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\"
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using static ShortcutGuide.Helpers.ResourceLoaderInstance;
|
||||
|
||||
@@ -25,6 +26,12 @@ namespace ShortcutGuide.Helpers
|
||||
{
|
||||
string path = Path.Combine(ManifestInterpreter.PathOfManifestFiles, $"Microsoft.PowerToys.{ManifestInterpreter.Language}.yml");
|
||||
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
Logger.LogWarning($"PowerToys manifest file not found: '{path}'. PowerToys-specific shortcuts will not appear in ShortcutGuide.");
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder content = new(File.ReadAllText(path));
|
||||
|
||||
const string populateStartString = "# <Populate start>";
|
||||
|
||||
@@ -99,7 +99,14 @@ namespace ShortcutGuide
|
||||
return;
|
||||
}
|
||||
|
||||
PowerToysShortcutsPopulator.Populate();
|
||||
try
|
||||
{
|
||||
PowerToysShortcutsPopulator.Populate();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to populate PowerToys shortcuts in manifest.", ex);
|
||||
}
|
||||
});
|
||||
CopyAndIndexGenerationThread.IsBackground = true;
|
||||
CopyAndIndexGenerationThread.Start();
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
<Content Remove="Assets\CopilotKey.png" />
|
||||
<Content Remove="Assets\ShortcutGuide\HeroImage.png" />
|
||||
<Content Remove="Assets\ShortcutGuide\ShortcutGuide.ico" />
|
||||
<Content Remove="Assets\ShortcutGuide\Manifests\*.yml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
"src\\common\\interop\\PowerToys.Interop.vcxproj",
|
||||
"src\\common\\logger\\logger.vcxproj",
|
||||
"src\\common\\version\\version.vcxproj",
|
||||
"src\\modules\\MouseUtils\\MouseJump.Common\\MouseJump.Common.csproj",
|
||||
"src\\modules\\ZoomIt\\ZoomItSettingsInterop\\ZoomItSettingsInterop.vcxproj",
|
||||
"src\\modules\\cmdpal\\CmdPalKeyboardService\\CmdPalKeyboardService.vcxproj",
|
||||
"src\\modules\\cmdpal\\CmdPalModuleInterface\\CmdPalModuleInterface.vcxproj",
|
||||
"src\\modules\\cmdpal\\Microsoft.CmdPal.Common\\Microsoft.CmdPal.Common.csproj",
|
||||
@@ -59,8 +57,7 @@
|
||||
"src\\modules\\cmdpal\\ext\\SamplePagesExtension\\SamplePagesExtension.csproj",
|
||||
"src\\modules\\cmdpal\\extensionsdk\\Microsoft.CommandPalette.Extensions.Toolkit\\Microsoft.CommandPalette.Extensions.Toolkit.csproj",
|
||||
"src\\modules\\cmdpal\\extensionsdk\\Microsoft.CommandPalette.Extensions\\Microsoft.CommandPalette.Extensions.vcxproj",
|
||||
"src\\modules\\powerdisplay\\PowerDisplay.Models\\PowerDisplay.Models.csproj",
|
||||
"src\\settings-ui\\Settings.UI.Library\\Settings.UI.Library.csproj"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,8 +76,13 @@ internal sealed partial class HttpResourceCacheHandler : DelegatingHandler
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var inflightKey in _inflightFetches.Keys)
|
||||
foreach (var (inflightKey, task) in _inflightFetches)
|
||||
{
|
||||
if (task.IsCompleted)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Uri.TryCreate(inflightKey, UriKind.Absolute, out var inflightUri))
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -208,7 +208,10 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
contextItemViewModel.Command.Id == ShowDetailsCommand.ShowDetailsCommandId))
|
||||
{
|
||||
var showDetailsCommand = new ShowDetailsCommand(Details);
|
||||
var showDetailsContextItem = new CommandContextItem(showDetailsCommand);
|
||||
var showDetailsContextItem = new CommandContextItem(showDetailsCommand)
|
||||
{
|
||||
Icon = showDetailsCommand.Icon,
|
||||
};
|
||||
var showDetailsContextItemViewModel = new CommandContextItemViewModel(showDetailsContextItem, PageContext);
|
||||
showDetailsContextItemViewModel.SlowInitializeProperties();
|
||||
UnsafeMoreCommands.Add(showDetailsContextItemViewModel);
|
||||
@@ -249,7 +252,10 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
}
|
||||
|
||||
var showDetailsCommand = new ShowDetailsCommand(Details);
|
||||
var showDetailsContextItem = new CommandContextItem(showDetailsCommand);
|
||||
var showDetailsContextItem = new CommandContextItem(showDetailsCommand)
|
||||
{
|
||||
Icon = showDetailsCommand.Icon,
|
||||
};
|
||||
var showDetailsContextItemViewModel = new CommandContextItemViewModel(showDetailsContextItem, PageContext);
|
||||
showDetailsContextItemViewModel.SlowInitializeProperties();
|
||||
UnsafeMoreCommands.Add(showDetailsContextItemViewModel);
|
||||
|
||||
@@ -1212,6 +1212,15 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Hide details.
|
||||
/// </summary>
|
||||
public static string HideDetailsCommand {
|
||||
get {
|
||||
return ResourceManager.GetString("HideDetailsCommand", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} of {1}.
|
||||
/// </summary>
|
||||
|
||||
@@ -307,6 +307,10 @@
|
||||
<value>Show details</value>
|
||||
<comment>Name for the command that shows details of an item</comment>
|
||||
</data>
|
||||
<data name="HideDetailsCommand" xml:space="preserve">
|
||||
<value>Hide details</value>
|
||||
<comment>Name for the command that hides details of an item</comment>
|
||||
</data>
|
||||
<data name="PinnedItemSuffix" xml:space="preserve">
|
||||
<value>Pinned</value>
|
||||
<comment>Suffix shown for pinned items in the dock</comment>
|
||||
|
||||
@@ -500,6 +500,18 @@ public partial class ShellViewModel : ObservableObject,
|
||||
WeakReferenceMessenger.Default.Send<GoHomeMessage>(new(withAnimation, focusSearch));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets navigation to the root page, clearing any transient state.
|
||||
/// Use when entering from a hotkey while the palette may already be
|
||||
/// showing a transient dock page.
|
||||
/// </summary>
|
||||
public void ResetToHome()
|
||||
{
|
||||
_currentlyTransient = false;
|
||||
_rootPageService.GoHome();
|
||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(new ExtensionObject<ICommand>(_rootPage)));
|
||||
}
|
||||
|
||||
public void GoBack(bool withAnimation = true, bool focusSearch = true)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<GoBackMessage>(new(withAnimation, focusSearch));
|
||||
|
||||
@@ -12,22 +12,39 @@ public sealed partial class ShowDetailsCommand : InvokableCommand
|
||||
{
|
||||
public static string ShowDetailsCommandId { get; } = "com.microsoft.cmdpal.showDetails";
|
||||
|
||||
private static IconInfo IconInfo { get; } = new IconInfo("\uF000"); // KnowledgeArticle Icon
|
||||
private static IconInfo ShowIcon { get; } = new IconInfo("\uF000"); // KnowledgeArticle Icon
|
||||
|
||||
private static IconInfo HideIcon { get; } = new IconInfo("\uED1A"); // Hide Icon
|
||||
|
||||
private DetailsViewModel Details { get; set; }
|
||||
|
||||
private bool _isDetailsVisible;
|
||||
|
||||
public ShowDetailsCommand(DetailsViewModel details)
|
||||
{
|
||||
Id = ShowDetailsCommandId;
|
||||
Name = UI.ViewModels.Properties.Resources.ShowDetailsCommand;
|
||||
Icon = IconInfo;
|
||||
Icon = ShowIcon;
|
||||
Details = details;
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
// Send the ShowDetailsMessage when the action is invoked
|
||||
WeakReferenceMessenger.Default.Send<ShowDetailsMessage>(new(Details));
|
||||
_isDetailsVisible = !_isDetailsVisible;
|
||||
|
||||
if (_isDetailsVisible)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<ShowDetailsMessage>(new(Details));
|
||||
Name = UI.ViewModels.Properties.Resources.HideDetailsCommand;
|
||||
Icon = HideIcon;
|
||||
}
|
||||
else
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<HideDetailsMessage>();
|
||||
Name = UI.ViewModels.Properties.Resources.ShowDetailsCommand;
|
||||
Icon = ShowIcon;
|
||||
}
|
||||
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using AdaptiveCards.ObjectModel.WinUI3;
|
||||
using AdaptiveCards.Rendering.WinUI3;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Automation;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
|
||||
@@ -101,11 +102,26 @@ public sealed partial class ContentFormControl : UserControl
|
||||
|
||||
// Use the Loaded event to ensure we focus after the card is in the visual tree
|
||||
_renderedCard.FrameworkElement.Loaded += OnFrameworkElementLoaded;
|
||||
|
||||
// Use LayoutUpdated to fix accessibility after the full visual tree is materialized.
|
||||
// Loaded fires too early — the Adaptive Card renderer may not have finished building
|
||||
// the control tree by then.
|
||||
_renderedCard.FrameworkElement.LayoutUpdated += OnFrameworkElementLayoutUpdated;
|
||||
}
|
||||
|
||||
_renderedCard.Action += Rendered_Action;
|
||||
}
|
||||
|
||||
private void OnFrameworkElementLayoutUpdated(object? sender, object e)
|
||||
{
|
||||
// Only fix once — unhook after first layout pass
|
||||
if (_renderedCard?.FrameworkElement is FrameworkElement element)
|
||||
{
|
||||
element.LayoutUpdated -= OnFrameworkElementLayoutUpdated;
|
||||
FixToggleAccessibilityNames(element);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFrameworkElementLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Unhook the event handler to avoid multiple registrations
|
||||
@@ -127,6 +143,109 @@ public sealed partial class ContentFormControl : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fixes missing AutomationProperties.Name on CheckBox and ToggleSwitch controls
|
||||
/// rendered by the Adaptive Cards library (AdaptiveCards.Rendering.WinUI3 v2.x).
|
||||
/// Without this fix, Narrator announces "space, checkbox, checked" instead of the
|
||||
/// actual setting label. This method walks the tree, finds CheckBox/ToggleSwitch
|
||||
/// controls missing an automation name, and sets it from the adjacent label TextBlock.
|
||||
/// </summary>
|
||||
private static void FixToggleAccessibilityNames(DependencyObject root)
|
||||
{
|
||||
var childCount = VisualTreeHelper.GetChildrenCount(root);
|
||||
for (var i = 0; i < childCount; i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(root, i);
|
||||
|
||||
if (child is CheckBox checkBox)
|
||||
{
|
||||
var existingName = AutomationProperties.GetName(checkBox);
|
||||
if (string.IsNullOrEmpty(existingName))
|
||||
{
|
||||
// In Adaptive Cards, the label TextBlock is a sibling to the CheckBox's
|
||||
// container within a shared Grid parent. Walk up to find that Grid, then
|
||||
// search for a TextBlock with actual text content.
|
||||
var labelText = FindAdjacentLabel(checkBox);
|
||||
if (!string.IsNullOrEmpty(labelText))
|
||||
{
|
||||
AutomationProperties.SetName(checkBox, labelText);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (child is ToggleSwitch toggleSwitch)
|
||||
{
|
||||
var existingName = AutomationProperties.GetName(toggleSwitch);
|
||||
if (string.IsNullOrEmpty(existingName))
|
||||
{
|
||||
var labelText = FindAdjacentLabel(toggleSwitch);
|
||||
if (!string.IsNullOrEmpty(labelText))
|
||||
{
|
||||
AutomationProperties.SetName(toggleSwitch, labelText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse into children
|
||||
FixToggleAccessibilityNames(child);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Walks up the visual tree from a control to find the nearest parent Grid,
|
||||
/// then searches that Grid's descendants for a TextBlock with non-whitespace text.
|
||||
/// This handles the Adaptive Cards layout where the label TextBlock is a sibling
|
||||
/// of the CheckBox's container within a shared Grid row.
|
||||
/// </summary>
|
||||
private static string? FindAdjacentLabel(FrameworkElement control)
|
||||
{
|
||||
// Walk up the tree to find the nearest Grid ancestor (the row container)
|
||||
DependencyObject? current = control;
|
||||
Grid? parentGrid = null;
|
||||
for (var depth = 0; depth < 10 && current != null; depth++)
|
||||
{
|
||||
current = VisualTreeHelper.GetParent(current);
|
||||
if (current is Grid grid)
|
||||
{
|
||||
// Find the grid that contains multiple children (the row container)
|
||||
if (VisualTreeHelper.GetChildrenCount(grid) > 1)
|
||||
{
|
||||
parentGrid = grid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (parentGrid == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Search the parent Grid for a TextBlock with actual text
|
||||
return FindFirstNonEmptyTextBlock(parentGrid);
|
||||
}
|
||||
|
||||
private static string? FindFirstNonEmptyTextBlock(DependencyObject root)
|
||||
{
|
||||
var childCount = VisualTreeHelper.GetChildrenCount(root);
|
||||
for (var i = 0; i < childCount; i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(root, i);
|
||||
|
||||
if (child is TextBlock tb && !string.IsNullOrWhiteSpace(tb.Text))
|
||||
{
|
||||
return tb.Text;
|
||||
}
|
||||
|
||||
var result = FindFirstNonEmptyTextBlock(child);
|
||||
if (result != null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Control? FindFirstFocusableElement(DependencyObject parent)
|
||||
{
|
||||
var childCount = VisualTreeHelper.GetChildrenCount(parent);
|
||||
|
||||
@@ -120,6 +120,7 @@ public sealed partial class DockItemControl : Control
|
||||
{
|
||||
control.UpdateTextVisibility();
|
||||
control.UpdateAlignment();
|
||||
control.UpdateCompactState();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -129,6 +129,9 @@ public sealed partial class DockWindow : WindowEx,
|
||||
overlappedPresenter.IsResizable = false;
|
||||
}
|
||||
|
||||
_hwnd = GetWindowHandle(this);
|
||||
_dock.OwnerHwnd = (nint)_hwnd;
|
||||
|
||||
// immediately when we're created: make sure to remove our window frame
|
||||
// and shadow. We don't _always_ get an Activated when we're first
|
||||
// created.
|
||||
@@ -139,9 +142,6 @@ public sealed partial class DockWindow : WindowEx,
|
||||
WeakReferenceMessenger.Default.Register<RequestShowPaletteAtMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<QuitMessage>(this);
|
||||
|
||||
_hwnd = GetWindowHandle(this);
|
||||
_dock.OwnerHwnd = (nint)_hwnd;
|
||||
|
||||
// Subclass the window to intercept messages
|
||||
//
|
||||
// Set up custom window procedure to listen for display changes
|
||||
|
||||
@@ -39,6 +39,58 @@
|
||||
</UserControl.Resources>
|
||||
<ScrollViewer>
|
||||
<StackPanel Width="420" Spacing="16">
|
||||
|
||||
<!-- Monitor Selector (hidden when only one monitor) -->
|
||||
<StackPanel
|
||||
x:Name="MonitorSelectorPanel"
|
||||
Spacing="8"
|
||||
Visibility="Collapsed">
|
||||
<TextBlock
|
||||
x:Uid="PinToDock_MonitorHeader"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
<ComboBox x:Name="MonitorComboBox" HorizontalAlignment="Stretch" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Dock Section Selector -->
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock
|
||||
x:Uid="PinToDock_SectionHeader"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
<tkcontrols:Segmented
|
||||
x:Name="SectionSegmented"
|
||||
HorizontalAlignment="Stretch"
|
||||
SelectedIndex="0"
|
||||
SelectionMode="Single">
|
||||
<tkcontrols:SegmentedItem x:Name="StartSegmentedItem" x:Uid="PinToDock_Start" />
|
||||
<tkcontrols:SegmentedItem x:Name="CenterSegmentedItem" x:Uid="PinToDock_Center" />
|
||||
<tkcontrols:SegmentedItem x:Name="EndSegmentedItem" x:Uid="PinToDock_End" />
|
||||
</tkcontrols:Segmented>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Label Options -->
|
||||
<StackPanel Spacing="4">
|
||||
<CheckBox
|
||||
x:Name="ShowTitleCheckBox"
|
||||
x:Uid="PinToDock_ShowTitle"
|
||||
Checked="OnLabelOptionChanged"
|
||||
IsChecked="True"
|
||||
Unchecked="OnLabelOptionChanged" />
|
||||
<CheckBox
|
||||
x:Name="ShowSubtitleCheckBox"
|
||||
x:Uid="PinToDock_ShowSubtitle"
|
||||
Checked="OnLabelOptionChanged"
|
||||
IsChecked="True"
|
||||
Unchecked="OnLabelOptionChanged" />
|
||||
</StackPanel>
|
||||
|
||||
<Rectangle
|
||||
Height="1"
|
||||
Margin="-24,0,-24,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
|
||||
<!-- Preview -->
|
||||
<StackPanel Spacing="8">
|
||||
<Border
|
||||
@@ -83,57 +135,6 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<Rectangle
|
||||
Height="1"
|
||||
Margin="-24,0,-24,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
|
||||
<!-- Dock Section Selector -->
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock
|
||||
x:Uid="PinToDock_SectionHeader"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
<tkcontrols:Segmented
|
||||
x:Name="SectionSegmented"
|
||||
HorizontalAlignment="Stretch"
|
||||
SelectedIndex="0"
|
||||
SelectionMode="Single">
|
||||
<tkcontrols:SegmentedItem x:Name="StartSegmentedItem" x:Uid="PinToDock_Start" />
|
||||
<tkcontrols:SegmentedItem x:Name="CenterSegmentedItem" x:Uid="PinToDock_Center" />
|
||||
<tkcontrols:SegmentedItem x:Name="EndSegmentedItem" x:Uid="PinToDock_End" />
|
||||
</tkcontrols:Segmented>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Monitor Selector (hidden when only one monitor) -->
|
||||
<StackPanel
|
||||
x:Name="MonitorSelectorPanel"
|
||||
Spacing="8"
|
||||
Visibility="Collapsed">
|
||||
<TextBlock
|
||||
x:Uid="PinToDock_MonitorHeader"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
<ComboBox x:Name="MonitorComboBox" HorizontalAlignment="Stretch" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Label Options -->
|
||||
<StackPanel Spacing="4">
|
||||
<CheckBox
|
||||
x:Name="ShowTitleCheckBox"
|
||||
x:Uid="PinToDock_ShowTitle"
|
||||
Checked="OnLabelOptionChanged"
|
||||
IsChecked="True"
|
||||
Unchecked="OnLabelOptionChanged" />
|
||||
<CheckBox
|
||||
x:Name="ShowSubtitleCheckBox"
|
||||
x:Uid="PinToDock_ShowSubtitle"
|
||||
Checked="OnLabelOptionChanged"
|
||||
IsChecked="True"
|
||||
Unchecked="OnLabelOptionChanged" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
|
||||
@@ -448,8 +448,9 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
if (isPage)
|
||||
{
|
||||
// If we're here, then the bound command was a page
|
||||
// of some kind. Let's pop the stack, show the window, and navigate to it.
|
||||
GoHome(false);
|
||||
// of some kind. Reset to root (clearing any transient dock state),
|
||||
// show the window, and navigate to it.
|
||||
ViewModel.ResetToHome();
|
||||
|
||||
WeakReferenceMessenger.Default.Send<ShowWindowMessage>(new(message.Hwnd));
|
||||
}
|
||||
|
||||
@@ -219,6 +219,7 @@
|
||||
AutomationProperties.AutomationId="{x:Bind Id, Mode=OneWay}"
|
||||
Click="SettingsCard_Click"
|
||||
DataContext="{x:Bind}"
|
||||
DataContextChanged="SettingsCard_DataContextChanged"
|
||||
Description="{x:Bind ExtensionSubtext, Mode=OneWay}"
|
||||
Header="{x:Bind DisplayName, Mode=OneWay}"
|
||||
IsClickEnabled="True">
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.WinUI.Controls;
|
||||
using ManagedCommon;
|
||||
@@ -9,6 +10,7 @@ using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Automation;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
|
||||
@@ -19,6 +21,7 @@ public sealed partial class ExtensionsPage : Page
|
||||
private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
|
||||
|
||||
private readonly SettingsViewModel? viewModel;
|
||||
private readonly Dictionary<string, WeakReference<SettingsCard>> _vmToCardMap = new();
|
||||
|
||||
public ExtensionsPage()
|
||||
{
|
||||
@@ -41,6 +44,38 @@ public sealed partial class ExtensionsPage : Page
|
||||
}
|
||||
}
|
||||
|
||||
private void SettingsCard_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
|
||||
{
|
||||
// Store the card reference keyed by Id (not the VM itself) to avoid leaking VM references
|
||||
if (sender is SettingsCard card && card.DataContext is ProviderSettingsViewModel newVm)
|
||||
{
|
||||
_vmToCardMap[newVm.Id] = new WeakReference<SettingsCard>(card);
|
||||
newVm.PropertyChanged += ProviderViewModel_PropertyChanged;
|
||||
|
||||
// Immediately update automation name in case DisplayName is already available
|
||||
if (card.Content is ToggleSwitch toggle && !string.IsNullOrEmpty(newVm.DisplayName))
|
||||
{
|
||||
AutomationProperties.SetName(toggle, newVm.DisplayName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ProviderViewModel_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
// When DisplayName changes, update the ToggleSwitch's automation name
|
||||
if (e.PropertyName == nameof(ProviderSettingsViewModel.DisplayName) && sender is ProviderSettingsViewModel vm && !string.IsNullOrEmpty(vm.DisplayName))
|
||||
{
|
||||
// Get the card reference from our map
|
||||
if (_vmToCardMap.TryGetValue(vm.Id, out var cardRef) && cardRef.TryGetTarget(out var card))
|
||||
{
|
||||
if (card.Content is ToggleSwitch toggle)
|
||||
{
|
||||
AutomationProperties.SetName(toggle, vm.DisplayName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFindInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
|
||||
{
|
||||
SearchBox?.Focus(FocusState.Keyboard);
|
||||
|
||||
@@ -1137,11 +1137,11 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<comment>Header for the preview section in the pin to dock dialog</comment>
|
||||
</data>
|
||||
<data name="PinToDock_SectionHeader.Text" xml:space="preserve">
|
||||
<value>Choose where to pin this command</value>
|
||||
<value>Position</value>
|
||||
<comment>Header for the dock section selector in the pin to dock dialog</comment>
|
||||
</data>
|
||||
<data name="PinToDock_MonitorHeader.Text" xml:space="preserve">
|
||||
<value>Choose which monitor</value>
|
||||
<value>Monitor</value>
|
||||
<comment>Header for the monitor selector in the pin to dock dialog</comment>
|
||||
</data>
|
||||
<data name="PinToDock_Start.Content" xml:space="preserve">
|
||||
|
||||
@@ -52,9 +52,9 @@ public class FallbackRemoteDesktopItemTests
|
||||
fallback.UpdateQuery(hostname);
|
||||
|
||||
// Assert
|
||||
var expectedTitle = string.Format(CultureInfo.CurrentCulture, OpenHostCompositeFormat, hostname);
|
||||
Assert.AreEqual(expectedTitle, fallback.Title);
|
||||
Assert.AreEqual(Resources.remotedesktop_title, fallback.Subtitle);
|
||||
Assert.AreEqual(Resources.remotedesktop_title, fallback.Title);
|
||||
var expectedSubtitleArbitrary = string.Format(CultureInfo.CurrentCulture, OpenHostCompositeFormat, hostname);
|
||||
Assert.AreEqual(expectedSubtitleArbitrary, fallback.Subtitle);
|
||||
|
||||
var command = fallback.Command as OpenRemoteDesktopCommand;
|
||||
Assert.IsNotNull(command);
|
||||
@@ -110,9 +110,9 @@ public class FallbackRemoteDesktopItemTests
|
||||
fallback.UpdateQuery(hostPort);
|
||||
|
||||
// Assert
|
||||
var expectedTitle = string.Format(CultureInfo.CurrentCulture, OpenHostCompositeFormat, hostPort);
|
||||
Assert.AreEqual(expectedTitle, fallback.Title);
|
||||
Assert.AreEqual(Resources.remotedesktop_title, fallback.Subtitle);
|
||||
Assert.AreEqual(Resources.remotedesktop_title, fallback.Title);
|
||||
var expectedSubtitleHostPort = string.Format(CultureInfo.CurrentCulture, OpenHostCompositeFormat, hostPort);
|
||||
Assert.AreEqual(expectedSubtitleHostPort, fallback.Subtitle);
|
||||
|
||||
var command = fallback.Command as OpenRemoteDesktopCommand;
|
||||
Assert.IsNotNull(command);
|
||||
@@ -131,9 +131,9 @@ public class FallbackRemoteDesktopItemTests
|
||||
fallback.UpdateQuery(hostPort);
|
||||
|
||||
// Assert
|
||||
var expectedTitle = string.Format(CultureInfo.CurrentCulture, OpenHostCompositeFormat, hostPort);
|
||||
Assert.AreEqual(expectedTitle, fallback.Title);
|
||||
Assert.AreEqual(Resources.remotedesktop_title, fallback.Subtitle);
|
||||
Assert.AreEqual(Resources.remotedesktop_title, fallback.Title);
|
||||
var expectedSubtitleHostPort = string.Format(CultureInfo.CurrentCulture, OpenHostCompositeFormat, hostPort);
|
||||
Assert.AreEqual(expectedSubtitleHostPort, fallback.Subtitle);
|
||||
|
||||
var command = fallback.Command as OpenRemoteDesktopCommand;
|
||||
Assert.IsNotNull(command);
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Pages;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class RemoteDesktopListPageTests
|
||||
{
|
||||
// Helper: create a RemoteDesktopListPage backed by mock connections.
|
||||
private static RemoteDesktopListPage CreatePage(params string[] connectionNames)
|
||||
{
|
||||
var settingsManager = new MockSettingsManager(connectionNames);
|
||||
var connectionsManager = new MockRdpConnectionsManager(settingsManager);
|
||||
return new RemoteDesktopListPage(connectionsManager);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EmptyQuery_ReturnsOnlyExistingConnections()
|
||||
{
|
||||
// Arrange
|
||||
var page = CreatePage("server1", "server2");
|
||||
|
||||
// Act
|
||||
page.UpdateSearchText(string.Empty, string.Empty);
|
||||
var items = page.GetItems();
|
||||
|
||||
// Assert — only the 2 preexisting connections, no arbitrary host item
|
||||
Assert.AreEqual(2, items.Length);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WhitespaceQuery_ReturnsOnlyExistingConnections()
|
||||
{
|
||||
// Arrange
|
||||
var page = CreatePage("server1");
|
||||
|
||||
// Act
|
||||
page.UpdateSearchText(string.Empty, " ");
|
||||
var items = page.GetItems();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(1, items.Length);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidHostname_NotMatchingConnection_PrependsArbitraryItem()
|
||||
{
|
||||
// Arrange
|
||||
var page = CreatePage("server1");
|
||||
|
||||
// Act
|
||||
page.UpdateSearchText(string.Empty, "test.corp");
|
||||
var items = page.GetItems();
|
||||
|
||||
// Assert — arbitrary item prepended, then the existing connection
|
||||
Assert.AreEqual(2, items.Length);
|
||||
var firstItem = items[0] as ConnectionListItem;
|
||||
Assert.IsNotNull(firstItem, "First item should be a ConnectionListItem for the arbitrary host");
|
||||
Assert.AreEqual("test.corp", firstItem.ConnectionName);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void QueryExactlyMatchingExistingConnection_NoArbitraryItem()
|
||||
{
|
||||
// Arrange
|
||||
var page = CreatePage("rdp-server");
|
||||
|
||||
// Act
|
||||
page.UpdateSearchText(string.Empty, "rdp-server");
|
||||
var items = page.GetItems();
|
||||
|
||||
// Assert — no extra item; count stays at 1
|
||||
Assert.AreEqual(1, items.Length);
|
||||
var item = items[0] as ConnectionListItem;
|
||||
Assert.IsNotNull(item);
|
||||
Assert.AreEqual("rdp-server", item.ConnectionName);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void InvalidHostname_ReturnsOnlyExistingConnections()
|
||||
{
|
||||
// Arrange
|
||||
var page = CreatePage("server1");
|
||||
|
||||
// Act
|
||||
page.UpdateSearchText(string.Empty, "!!!invalid");
|
||||
var items = page.GetItems();
|
||||
|
||||
// Assert — no arbitrary item for an invalid hostname
|
||||
Assert.AreEqual(1, items.Length);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidHostnameWithPort_ArbitraryItemIncludesPort()
|
||||
{
|
||||
// Arrange
|
||||
var page = CreatePage("server1");
|
||||
|
||||
// Act
|
||||
page.UpdateSearchText(string.Empty, "localhost:3389");
|
||||
var items = page.GetItems();
|
||||
|
||||
// Assert — full host:port string preserved as the ConnectionName
|
||||
Assert.AreEqual(2, items.Length);
|
||||
var firstItem = items[0] as ConnectionListItem;
|
||||
Assert.IsNotNull(firstItem);
|
||||
Assert.AreEqual("localhost:3389", firstItem.ConnectionName);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SequentialCalls_ValidThenEmpty_ClearsArbitraryItem()
|
||||
{
|
||||
// Arrange
|
||||
var page = CreatePage("server1");
|
||||
|
||||
// Act — first call adds arbitrary item
|
||||
page.UpdateSearchText(string.Empty, "test.corp");
|
||||
var itemsAfterValid = page.GetItems();
|
||||
|
||||
// Assert — arbitrary item present
|
||||
Assert.AreEqual(2, itemsAfterValid.Length);
|
||||
var firstItem = itemsAfterValid[0] as ConnectionListItem;
|
||||
Assert.IsNotNull(firstItem);
|
||||
Assert.AreEqual("test.corp", firstItem.ConnectionName);
|
||||
|
||||
// Act — second call clears it
|
||||
page.UpdateSearchText("test.corp", string.Empty);
|
||||
var itemsAfterEmpty = page.GetItems();
|
||||
|
||||
// Assert — back to only existing connections
|
||||
Assert.AreEqual(1, itemsAfterEmpty.Length);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidHostname_NoExistingConnections_ReturnsSingleArbitraryItem()
|
||||
{
|
||||
// Arrange — no preexisting connections
|
||||
var page = CreatePage();
|
||||
|
||||
// Act
|
||||
page.UpdateSearchText(string.Empty, "alpha.corp");
|
||||
var items = page.GetItems();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(1, items.Length);
|
||||
var firstItem = items[0] as ConnectionListItem;
|
||||
Assert.IsNotNull(firstItem);
|
||||
Assert.AreEqual("alpha.corp", firstItem.ConnectionName);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IPv4Address_ReturnsArbitraryItem()
|
||||
{
|
||||
// Arrange
|
||||
var page = CreatePage();
|
||||
|
||||
// Act
|
||||
page.UpdateSearchText(string.Empty, "192.168.1.100");
|
||||
var items = page.GetItems();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(1, items.Length);
|
||||
var firstItem = items[0] as ConnectionListItem;
|
||||
Assert.IsNotNull(firstItem);
|
||||
Assert.AreEqual("192.168.1.100", firstItem.ConnectionName);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IPv4AddressWithPort_ReturnsArbitraryItemWithPort()
|
||||
{
|
||||
// Arrange
|
||||
var page = CreatePage();
|
||||
|
||||
// Act
|
||||
page.UpdateSearchText(string.Empty, "192.168.1.100:3390");
|
||||
var items = page.GetItems();
|
||||
|
||||
// Assert — full IP:port string preserved
|
||||
Assert.AreEqual(1, items.Length);
|
||||
var firstItem = items[0] as ConnectionListItem;
|
||||
Assert.IsNotNull(firstItem);
|
||||
Assert.AreEqual("192.168.1.100:3390", firstItem.ConnectionName);
|
||||
}
|
||||
}
|
||||
@@ -30,8 +30,7 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, IDispo
|
||||
private const uint HardQueryCookie = 10;
|
||||
private static readonly NoOpCommand BaseCommandWithId = new() { Id = CommandId };
|
||||
|
||||
private readonly CompositeFormat _fallbackItemSearchPageTitleFormat = CompositeFormat.Parse(Resources.Indexer_fallback_searchPage_title);
|
||||
private readonly CompositeFormat _fallbackItemSearchSubtitleMultipleResults = CompositeFormat.Parse(Resources.Indexer_Fallback_MultipleResults_Subtitle);
|
||||
private readonly CompositeFormat _fallbackItemSearchSubtitleFormat = CompositeFormat.Parse(Resources.Indexer_fallback_searchPage_title);
|
||||
private readonly Lock _querySwitchLock = new();
|
||||
private readonly Lock _resultLock = new();
|
||||
|
||||
@@ -152,8 +151,8 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, IDispo
|
||||
var indexerPage = new IndexerPage(query);
|
||||
|
||||
var set = UpdateResultForCurrentQuery(
|
||||
string.Format(CultureInfo.CurrentCulture, _fallbackItemSearchPageTitleFormat, query),
|
||||
string.Format(CultureInfo.CurrentCulture, _fallbackItemSearchSubtitleMultipleResults),
|
||||
Resources.IndexerCommandsProvider_DisplayName,
|
||||
string.Format(CultureInfo.CurrentCulture, _fallbackItemSearchSubtitleFormat, query),
|
||||
Icons.FileExplorerIcon,
|
||||
indexerPage,
|
||||
MoreCommands,
|
||||
|
||||
@@ -193,7 +193,7 @@
|
||||
<value>Search files</value>
|
||||
</data>
|
||||
<data name="Indexer_fallback_searchPage_title" xml:space="preserve">
|
||||
<value>Search for "{0}" in files</value>
|
||||
<value>Search for {0} in files</value>
|
||||
</data>
|
||||
<data name="Indexer_NoResultsMessage" xml:space="preserve">
|
||||
<value>No items found</value>
|
||||
|
||||
@@ -212,6 +212,11 @@ internal sealed partial class GPUStats : PerformanceCounterSourceBase, IDisposab
|
||||
|
||||
internal string CreateGPUImageUrl(int gpuChartIndex)
|
||||
{
|
||||
if (_stats.Count <= gpuChartIndex)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return ChartHelper.CreateImageUrl(_stats[gpuChartIndex].GpuChartValues, ChartHelper.ChartType.GPU);
|
||||
}
|
||||
|
||||
|
||||
@@ -270,8 +270,8 @@ internal sealed partial class PerformanceWidgetsPage : OnLoadStaticListPage, IDi
|
||||
};
|
||||
|
||||
return _batteryItem is not null
|
||||
? new[] { _cpuItem!, _memoryItem!, _networkDownItem!, _networkUpItem!, _gpuItem!, _batteryItem! }
|
||||
: new[] { _cpuItem!, _memoryItem!, _networkDownItem!, _networkUpItem!, _gpuItem! };
|
||||
? new[] { _cpuItem!, _memoryItem!, _networkUpItem!, _networkDownItem!, _gpuItem!, _batteryItem! }
|
||||
: new[] { _cpuItem!, _memoryItem!, _networkUpItem!, _networkDownItem!, _gpuItem! };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ internal sealed class SettingsManager : JsonSettingsManager
|
||||
{
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
Directory.CreateDirectory(directory);
|
||||
return Path.Combine(directory, "settings.json");
|
||||
return Path.Combine(directory, $"{Namespace}.settings.json");
|
||||
}
|
||||
|
||||
public SettingsManager()
|
||||
|
||||
@@ -58,7 +58,7 @@ internal sealed class KeyboardManagerModuleCommandProvider : ModuleCommandProvid
|
||||
|
||||
if (!File.Exists(settingsPath))
|
||||
{
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(settingsPath);
|
||||
@@ -72,9 +72,9 @@ internal sealed class KeyboardManagerModuleCommandProvider : ModuleCommandProvid
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If we can't read the setting, default to not showing the command
|
||||
// If we can't read the setting, default to showing the command
|
||||
}
|
||||
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,8 +79,8 @@ internal sealed partial class FallbackRemoteDesktopItem : FallbackCommandItem
|
||||
{
|
||||
var connectionName = query.Trim();
|
||||
Command = new OpenRemoteDesktopCommand(connectionName);
|
||||
Title = string.Format(CultureInfo.CurrentCulture, RemoteDesktopOpenHostFormat, connectionName);
|
||||
Subtitle = Resources.remotedesktop_title;
|
||||
Title = Resources.remotedesktop_title;
|
||||
Subtitle = string.Format(CultureInfo.CurrentCulture, RemoteDesktopOpenHostFormat, connectionName);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
@@ -10,9 +12,16 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Pages;
|
||||
|
||||
internal sealed partial class RemoteDesktopListPage : ListPage
|
||||
internal sealed partial class RemoteDesktopListPage : DynamicListPage
|
||||
{
|
||||
private static readonly UriHostNameType[] ValidUriHostNameTypes = [
|
||||
UriHostNameType.IPv6,
|
||||
UriHostNameType.IPv4,
|
||||
UriHostNameType.Dns,
|
||||
];
|
||||
|
||||
private readonly IRdpConnectionsManager _rdpConnectionsManager;
|
||||
private ConnectionListItem? _arbitraryHostItem;
|
||||
|
||||
public RemoteDesktopListPage(IRdpConnectionsManager rdpConnectionsManager)
|
||||
{
|
||||
@@ -23,5 +32,68 @@ internal sealed partial class RemoteDesktopListPage : ListPage
|
||||
_rdpConnectionsManager = rdpConnectionsManager;
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems() => _rdpConnectionsManager.Connections.ToArray();
|
||||
public override void UpdateSearchText(string oldSearch, string newSearch)
|
||||
{
|
||||
var query = newSearch?.Trim() ?? string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
_arbitraryHostItem = null;
|
||||
RaiseItemsChanged(0);
|
||||
return;
|
||||
}
|
||||
|
||||
var connections = _rdpConnectionsManager.Connections.Where(w => !string.IsNullOrWhiteSpace(w.ConnectionName));
|
||||
var existingMatch = ConnectionHelpers.FindConnection(query, connections);
|
||||
|
||||
if (existingMatch is not null && string.Equals(existingMatch.ConnectionName, query, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Exact match — no need for an arbitrary host item
|
||||
_arbitraryHostItem = null;
|
||||
}
|
||||
else if (IsValidHostname(query))
|
||||
{
|
||||
_arbitraryHostItem = new ConnectionListItem(query);
|
||||
}
|
||||
else
|
||||
{
|
||||
_arbitraryHostItem = null;
|
||||
}
|
||||
|
||||
RaiseItemsChanged(0);
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems()
|
||||
{
|
||||
var connections = _rdpConnectionsManager.Connections.ToArray();
|
||||
|
||||
if (_arbitraryHostItem is null)
|
||||
{
|
||||
return connections;
|
||||
}
|
||||
|
||||
// Prepend the arbitrary host item so it appears at the top
|
||||
var result = new IListItem[connections.Length + 1];
|
||||
result[0] = _arbitraryHostItem;
|
||||
connections.CopyTo(result, 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool IsValidHostname(string query)
|
||||
{
|
||||
// Strip port suffix (e.g. "host:3389") before validation,
|
||||
// since Uri.CheckHostName does not accept host:port strings.
|
||||
var hostForValidation = query;
|
||||
var lastColon = hostForValidation.LastIndexOf(':');
|
||||
if (lastColon > 0 && lastColon < hostForValidation.Length - 1)
|
||||
{
|
||||
var portPart = hostForValidation.Substring(lastColon + 1);
|
||||
if (ushort.TryParse(portPart, out _))
|
||||
{
|
||||
hostForValidation = hostForValidation.Substring(0, lastColon);
|
||||
}
|
||||
}
|
||||
|
||||
return ValidUriHostNameTypes.Contains(Uri.CheckHostName(hostForValidation));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,6 @@ internal sealed partial class FallbackExecuteSearchItem : FallbackCommandItem
|
||||
var isEmpty = string.IsNullOrEmpty(query);
|
||||
_executeItem.Name = isEmpty ? string.Empty : Resources.open_in_default_browser;
|
||||
Title = isEmpty ? string.Empty : UpdateBrowserName(_browserInfoService);
|
||||
Subtitle = string.Format(CultureInfo.CurrentCulture, SubtitleText, query);
|
||||
Subtitle = isEmpty ? string.Empty : string.Format(CultureInfo.CurrentCulture, SubtitleText, query);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,10 +56,9 @@ internal sealed partial class FallbackOpenURLItem : FallbackCommandItem
|
||||
_executeItem.Url = query;
|
||||
_executeItem.Name = string.IsNullOrEmpty(query) ? string.Empty : Resources.open_in_default_browser;
|
||||
|
||||
Title = string.Format(CultureInfo.CurrentCulture, PluginOpenURL, query);
|
||||
|
||||
var browserName = _browserInfoService.GetDefaultBrowser()?.Name;
|
||||
Subtitle = string.IsNullOrWhiteSpace(browserName) ? Resources.open_in_default_browser : string.Format(CultureInfo.CurrentCulture, PluginOpenUrlInBrowser, browserName);
|
||||
Title = string.IsNullOrWhiteSpace(browserName) ? Resources.open_in_default_browser : string.Format(CultureInfo.CurrentCulture, PluginOpenUrlInBrowser, browserName);
|
||||
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpenURL, query);
|
||||
}
|
||||
|
||||
private static bool IsValidUrl(string url)
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
<value>Search the web in {0}</value>
|
||||
</data>
|
||||
<data name="plugin_open_url" xml:space="preserve">
|
||||
<value>Open "{0}"</value>
|
||||
<value>Open {0}</value>
|
||||
</data>
|
||||
<data name="plugin_open_url_in_browser" xml:space="preserve">
|
||||
<value>Open url in {0}</value>
|
||||
@@ -179,7 +179,7 @@
|
||||
<value>Settings</value>
|
||||
</data>
|
||||
<data name="web_search_fallback_subtitle" xml:space="preserve">
|
||||
<value>Search for "{0}"</value>
|
||||
<value>Search for {0}</value>
|
||||
</data>
|
||||
<data name="open_url_fallback_title" xml:space="preserve">
|
||||
<value>Open URL</value>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CmdPal.Ext.WindowsSettings.Commands;
|
||||
using Microsoft.CmdPal.Ext.WindowsSettings.Helpers;
|
||||
using Microsoft.CmdPal.Ext.WindowsSettings.Properties;
|
||||
@@ -17,7 +18,7 @@ internal sealed partial class FallbackWindowsSettingsItem : FallbackCommandItem
|
||||
|
||||
private readonly Classes.WindowsSettings _windowsSettings;
|
||||
|
||||
private readonly string _title = Resources.settings_fallback_title;
|
||||
private static readonly CompositeFormat _titleFormat = CompositeFormat.Parse(Resources.settings_fallback_title);
|
||||
private readonly string _subtitle = Resources.settings_fallback_subtitle;
|
||||
|
||||
public FallbackWindowsSettingsItem(Classes.WindowsSettings windowsSettings)
|
||||
@@ -78,9 +79,9 @@ internal sealed partial class FallbackWindowsSettingsItem : FallbackCommandItem
|
||||
// We found more than one result. Make our command take
|
||||
// us to the Windows Settings search page, prepopulated with this search.
|
||||
var settingsPage = new WindowsSettingsListPage(_windowsSettings, query);
|
||||
Title = string.Format(CultureInfo.CurrentCulture, _title, query);
|
||||
Title = _subtitle;
|
||||
Icon = Icons.WindowsSettingsIcon;
|
||||
Subtitle = _subtitle;
|
||||
Subtitle = string.Format(CultureInfo.CurrentCulture, _titleFormat, query);
|
||||
Command = settingsPage;
|
||||
|
||||
return;
|
||||
|
||||
@@ -2093,7 +2093,7 @@
|
||||
<value>Navigate to specific Windows settings</value>
|
||||
</data>
|
||||
<data name="settings_fallback_title" xml:space="preserve">
|
||||
<value>Search for "{0}" in Windows settings</value>
|
||||
<value>Search for {0}</value>
|
||||
</data>
|
||||
<data name="settings_fallback_subtitle" xml:space="preserve">
|
||||
<value>Search Windows settings for this device</value>
|
||||
|
||||
@@ -60,7 +60,7 @@ private:
|
||||
Hotkey m_editorHotkey = { .key = 0 };
|
||||
|
||||
// Whether to use the new WinUI3 editor
|
||||
bool m_useNewEditor = false;
|
||||
bool m_useNewEditor = true;
|
||||
|
||||
ULONGLONG m_lastHotkeyTime = 0;
|
||||
|
||||
@@ -196,11 +196,11 @@ private:
|
||||
try
|
||||
{
|
||||
auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
|
||||
m_useNewEditor = propertiesObject.GetNamedBoolean(JSON_KEY_USE_NEW_EDITOR, false);
|
||||
m_useNewEditor = propertiesObject.GetNamedBoolean(JSON_KEY_USE_NEW_EDITOR, true);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::warn("Failed to parse useNewEditor setting, defaulting to false");
|
||||
Logger::warn("Failed to parse useNewEditor setting, defaulting to true");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ namespace Microsoft.PowerToys.Run.Plugin.Calculator.UnitTests
|
||||
[DataRow("=2^96", "Result value was either too large or too small for a decimal number")]
|
||||
[DataRow("=+()", "Calculation result is not a valid number (NaN)")]
|
||||
[DataRow("=[10,10]", "Unsupported use of square brackets")]
|
||||
[DataRow("=sqrt(-1)", "Complex numbers are not supported")]
|
||||
[DataRow("=5/0", "Expression contains division by zero")]
|
||||
[DataRow("=5 / 0", "Expression contains division by zero")]
|
||||
[DataRow("10+(8*9)/0+7", "Expression contains division by zero")]
|
||||
@@ -63,6 +64,7 @@ namespace Microsoft.PowerToys.Run.Plugin.Calculator.UnitTests
|
||||
[DataRow("2^96")]
|
||||
[DataRow("+()")]
|
||||
[DataRow("[10,10]")]
|
||||
[DataRow("sqrt(-1)")]
|
||||
[DataRow("5/0")]
|
||||
[DataRow("5 / 0")]
|
||||
[DataRow("10+(8*9)/0+7")]
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using Mages.Core;
|
||||
@@ -124,6 +125,11 @@ namespace Microsoft.PowerToys.Run.Plugin.Calculator
|
||||
return Properties.Resources.wox_plugin_calculator_double_array_returned;
|
||||
}
|
||||
|
||||
if (result is Complex)
|
||||
{
|
||||
return Properties.Resources.wox_plugin_calculator_complex_number_returned;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,15 @@ namespace Microsoft.PowerToys.Run.Plugin.Calculator.Properties {
|
||||
return ResourceManager.GetString("wox_plugin_calculator_calculation_failed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Complex numbers are not supported.
|
||||
/// </summary>
|
||||
public static string wox_plugin_calculator_complex_number_returned {
|
||||
get {
|
||||
return ResourceManager.GetString("wox_plugin_calculator_complex_number_returned", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Copy failed, please try later.
|
||||
|
||||
@@ -141,6 +141,9 @@
|
||||
<data name="wox_plugin_calculator_double_array_returned" xml:space="preserve">
|
||||
<value>Unsupported use of square brackets</value>
|
||||
</data>
|
||||
<data name="wox_plugin_calculator_complex_number_returned" xml:space="preserve">
|
||||
<value>Complex numbers are not supported</value>
|
||||
</data>
|
||||
<data name="wox_plugin_calculator_not_covert_to_decimal" xml:space="preserve">
|
||||
<value>Result value was either too large or too small for a decimal number</value>
|
||||
</data>
|
||||
@@ -187,4 +190,4 @@
|
||||
<value>Gradians</value>
|
||||
<comment>Text for angle unit.</comment>
|
||||
</data>
|
||||
</root>
|
||||
</root>
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using PowerDisplay.Common.Services;
|
||||
|
||||
namespace PowerDisplay.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class CrashDetectionScopeTests
|
||||
{
|
||||
private string _tempDir = string.Empty;
|
||||
private string _lockPath = string.Empty;
|
||||
|
||||
[TestInitialize]
|
||||
public void SetUp()
|
||||
{
|
||||
_tempDir = Path.Combine(Path.GetTempPath(), $"pd-scope-test-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_tempDir);
|
||||
_lockPath = Path.Combine(_tempDir, "discovery.lock");
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
public void TearDown()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(_tempDir))
|
||||
{
|
||||
Directory.Delete(_tempDir, recursive: true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// best-effort cleanup
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeProcessExitHook : IProcessExitHook
|
||||
{
|
||||
public List<EventHandler> Handlers { get; } = new();
|
||||
|
||||
public void Subscribe(EventHandler handler) => Handlers.Add(handler);
|
||||
|
||||
public void Unsubscribe(EventHandler handler) => Handlers.Remove(handler);
|
||||
|
||||
public void RaiseExit()
|
||||
{
|
||||
// Copy to tolerate handlers that mutate the list (e.g. self-unsubscribe).
|
||||
foreach (var h in Handlers.ToList())
|
||||
{
|
||||
h(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Begin_WritesLockFileAtomically()
|
||||
{
|
||||
var hook = new FakeProcessExitHook();
|
||||
|
||||
using var scope = CrashDetectionScope.Begin(_lockPath, hook);
|
||||
|
||||
Assert.IsTrue(File.Exists(_lockPath), "lock file should exist after Begin");
|
||||
Assert.IsFalse(File.Exists(_lockPath + ".tmp"), "temp file should not linger");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Begin_SubscribesToProcessExit()
|
||||
{
|
||||
var hook = new FakeProcessExitHook();
|
||||
|
||||
using var scope = CrashDetectionScope.Begin(_lockPath, hook);
|
||||
|
||||
Assert.AreEqual(1, hook.Handlers.Count, "Begin should subscribe exactly one ProcessExit handler");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Dispose_UnsubscribesFromProcessExit()
|
||||
{
|
||||
var hook = new FakeProcessExitHook();
|
||||
var scope = CrashDetectionScope.Begin(_lockPath, hook);
|
||||
|
||||
scope.Dispose();
|
||||
|
||||
Assert.AreEqual(0, hook.Handlers.Count, "Dispose should unsubscribe the ProcessExit handler");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Dispose_DeletesLockFile()
|
||||
{
|
||||
var hook = new FakeProcessExitHook();
|
||||
var scope = CrashDetectionScope.Begin(_lockPath, hook);
|
||||
Assert.IsTrue(File.Exists(_lockPath));
|
||||
|
||||
scope.Dispose();
|
||||
|
||||
Assert.IsFalse(File.Exists(_lockPath), "Dispose should delete the lock file");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ProcessExitFired_BeforeDispose_DeletesLock()
|
||||
{
|
||||
var hook = new FakeProcessExitHook();
|
||||
var scope = CrashDetectionScope.Begin(_lockPath, hook);
|
||||
Assert.IsTrue(File.Exists(_lockPath));
|
||||
|
||||
// Simulate Environment.Exit firing ProcessExit while the scope is still alive
|
||||
// (i.e. discovery is mid-flight, Dispose hasn't run).
|
||||
hook.RaiseExit();
|
||||
|
||||
Assert.IsFalse(File.Exists(_lockPath), "ProcessExit handler should best-effort delete the lock");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ProcessExitFired_AfterDispose_DoesNothing()
|
||||
{
|
||||
var hook = new FakeProcessExitHook();
|
||||
var scope = CrashDetectionScope.Begin(_lockPath, hook);
|
||||
|
||||
scope.Dispose();
|
||||
Assert.IsFalse(File.Exists(_lockPath));
|
||||
|
||||
// Even if a stale handler somehow fires after Dispose, it must not throw.
|
||||
// (In practice Dispose unsubscribed it; this guards against ordering races.)
|
||||
hook.RaiseExit();
|
||||
|
||||
Assert.IsFalse(File.Exists(_lockPath), "lock should remain deleted");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Dispose_AfterProcessExit_DoesNotThrow()
|
||||
{
|
||||
var hook = new FakeProcessExitHook();
|
||||
var scope = CrashDetectionScope.Begin(_lockPath, hook);
|
||||
|
||||
hook.RaiseExit();
|
||||
Assert.IsFalse(File.Exists(_lockPath));
|
||||
|
||||
// Late Dispose (e.g. from a finally block on a still-running thread) must be safe.
|
||||
scope.Dispose();
|
||||
|
||||
Assert.IsFalse(File.Exists(_lockPath));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ProcessExitFired_LockFileMissing_DoesNotThrow()
|
||||
{
|
||||
var hook = new FakeProcessExitHook();
|
||||
var scope = CrashDetectionScope.Begin(_lockPath, hook);
|
||||
|
||||
// Simulate the lock being removed by something else first.
|
||||
File.Delete(_lockPath);
|
||||
|
||||
// No exception expected — ProcessExit handlers must not throw or they'd
|
||||
// disrupt the CLR shutdown sequence.
|
||||
hook.RaiseExit();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Dispose_IsIdempotent()
|
||||
{
|
||||
var hook = new FakeProcessExitHook();
|
||||
var scope = CrashDetectionScope.Begin(_lockPath, hook);
|
||||
|
||||
scope.Dispose();
|
||||
scope.Dispose();
|
||||
|
||||
Assert.AreEqual(0, hook.Handlers.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MultipleScopes_DoNotShareState()
|
||||
{
|
||||
var hook1 = new FakeProcessExitHook();
|
||||
var path1 = Path.Combine(_tempDir, "lock1");
|
||||
var path2 = Path.Combine(_tempDir, "lock2");
|
||||
|
||||
var scope1 = CrashDetectionScope.Begin(path1, hook1);
|
||||
scope1.Dispose();
|
||||
|
||||
var hook2 = new FakeProcessExitHook();
|
||||
using var scope2 = CrashDetectionScope.Begin(path2, hook2);
|
||||
|
||||
Assert.AreEqual(0, hook1.Handlers.Count, "first scope's handler should be unsubscribed");
|
||||
Assert.AreEqual(1, hook2.Handlers.Count, "second scope should subscribe on its own hook");
|
||||
Assert.IsFalse(File.Exists(path1));
|
||||
Assert.IsTrue(File.Exists(path2));
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,22 @@ namespace PowerDisplay.Common.Services
|
||||
/// PowerDisplay.exe startup CrashRecovery detects the orphan and disables the module.
|
||||
///
|
||||
/// On normal completion or .NET exception, Dispose() removes the lock.
|
||||
///
|
||||
/// As a safety net for cooperative shutdowns (Runner's TerminateApp NamedPipe, Terminate
|
||||
/// named event, tray-quit, Runner-exit detection, PowerToys upgrade), Begin() also
|
||||
/// subscribes to <see cref="AppDomain.ProcessExit"/>. Those paths currently call
|
||||
/// <see cref="Environment.Exit"/>, which terminates the process without unwinding
|
||||
/// background-thread finally blocks — so without this hook the lock would orphan and
|
||||
/// the next startup would false-positive. ProcessExit fires for Environment.Exit but
|
||||
/// NOT for FailFast / BSOD / external TerminateProcess, which is exactly the partition
|
||||
/// we want: cooperative exit → best-effort delete the lock; involuntary kill → leave the
|
||||
/// lock for Phase 0 to detect.
|
||||
/// </summary>
|
||||
public sealed partial class CrashDetectionScope : IDisposable
|
||||
{
|
||||
private readonly string _lockPath;
|
||||
private readonly IProcessExitHook _processExitHook;
|
||||
private readonly EventHandler _processExitHandler;
|
||||
private int _disposedFlag;
|
||||
|
||||
/// <summary>
|
||||
@@ -35,9 +47,13 @@ namespace PowerDisplay.Common.Services
|
||||
/// </summary>
|
||||
/// <param name="lockPath">Override the default lock path. Defaults to
|
||||
/// <see cref="PathConstants.DiscoveryLockPath"/>. Test code passes a temp path.</param>
|
||||
public static CrashDetectionScope Begin(string? lockPath = null)
|
||||
/// <param name="processExitHook">Override the default <see cref="AppDomain.ProcessExit"/>
|
||||
/// subscription. Test code passes a fake so it can simulate process exit without
|
||||
/// terminating the test runner.</param>
|
||||
public static CrashDetectionScope Begin(string? lockPath = null, IProcessExitHook? processExitHook = null)
|
||||
{
|
||||
var path = lockPath ?? PathConstants.DiscoveryLockPath;
|
||||
var hook = processExitHook ?? AppDomainProcessExitHook.Instance;
|
||||
|
||||
var dir = Path.GetDirectoryName(path);
|
||||
if (!string.IsNullOrEmpty(dir))
|
||||
@@ -91,12 +107,39 @@ namespace PowerDisplay.Common.Services
|
||||
}
|
||||
|
||||
Logger.LogInfo($"CrashDetectionScope: lock written at {path}");
|
||||
return new CrashDetectionScope(path);
|
||||
var scope = new CrashDetectionScope(path, hook);
|
||||
hook.Subscribe(scope._processExitHandler);
|
||||
return scope;
|
||||
}
|
||||
|
||||
private CrashDetectionScope(string lockPath)
|
||||
private CrashDetectionScope(string lockPath, IProcessExitHook processExitHook)
|
||||
{
|
||||
_lockPath = lockPath;
|
||||
_processExitHook = processExitHook;
|
||||
_processExitHandler = OnProcessExit;
|
||||
}
|
||||
|
||||
// Fired when the process is exiting cooperatively (Environment.Exit). Best-effort delete
|
||||
// the lock so the next startup does not false-positive. Must not throw: ProcessExit
|
||||
// handlers run inside the CLR shutdown sequence and any exception here disrupts it.
|
||||
// Interlocked guards against a race with Dispose() — only one of the two wins and does
|
||||
// the delete; the other returns early.
|
||||
private void OnProcessExit(object? sender, EventArgs e)
|
||||
{
|
||||
if (Interlocked.Exchange(ref _disposedFlag, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
File.Delete(_lockPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Worst case: false-positive on next start — same as today's behavior, so this
|
||||
// hook is strictly an improvement.
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -106,6 +149,8 @@ namespace PowerDisplay.Common.Services
|
||||
return;
|
||||
}
|
||||
|
||||
_processExitHook.Unsubscribe(_processExitHandler);
|
||||
|
||||
try
|
||||
{
|
||||
File.Delete(_lockPath);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace PowerDisplay.Common.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Abstraction over <see cref="AppDomain.ProcessExit"/> so <see cref="CrashDetectionScope"/>
|
||||
/// can be unit-tested without invoking real process termination.
|
||||
/// </summary>
|
||||
public interface IProcessExitHook
|
||||
{
|
||||
void Subscribe(EventHandler handler);
|
||||
|
||||
void Unsubscribe(EventHandler handler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default production implementation that bridges to <see cref="AppDomain.ProcessExit"/>.
|
||||
/// </summary>
|
||||
internal sealed class AppDomainProcessExitHook : IProcessExitHook
|
||||
{
|
||||
public static readonly AppDomainProcessExitHook Instance = new AppDomainProcessExitHook();
|
||||
|
||||
private AppDomainProcessExitHook()
|
||||
{
|
||||
}
|
||||
|
||||
public void Subscribe(EventHandler handler) => AppDomain.CurrentDomain.ProcessExit += handler;
|
||||
|
||||
public void Unsubscribe(EventHandler handler) => AppDomain.CurrentDomain.ProcessExit -= handler;
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
public HotkeySettings EditorShortcut { get; set; }
|
||||
|
||||
[JsonPropertyName("useNewEditor")]
|
||||
public bool UseNewEditor { get; set; }
|
||||
public bool UseNewEditor { get; set; } = true;
|
||||
|
||||
public string ToJsonString()
|
||||
{
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 20 KiB |
@@ -36,7 +36,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.ViewModel
|
||||
(PowerToysModules.MouseUtils, false),
|
||||
(PowerToysModules.MouseWithoutBorders, false),
|
||||
(PowerToysModules.Peek, false),
|
||||
(PowerToysModules.PowerDisplay, true),
|
||||
(PowerToysModules.PowerDisplay, false),
|
||||
(PowerToysModules.PowerRename, false),
|
||||
(PowerToysModules.Run, false),
|
||||
(PowerToysModules.QuickAccent, false),
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<ContentDialog
|
||||
x:Class="Microsoft.PowerToys.Settings.UI.Views.DangerousFeatureWarningDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
DefaultButton="Close"
|
||||
Style="{StaticResource DefaultContentDialogStyle}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock
|
||||
x:Name="WarningHeader"
|
||||
FontWeight="Bold"
|
||||
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock x:Name="WarningDescription" TextWrapping="Wrap" />
|
||||
<ItemsControl x:Name="WarningList" Margin="20,0,0,0">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Spacing="4" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="x:String">
|
||||
<TextBlock Text="{x:Bind}" TextWrapping="Wrap" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<TextBlock
|
||||
x:Name="WarningConfirm"
|
||||
FontWeight="SemiBold"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</ContentDialog>
|
||||
@@ -1,63 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.Windows.ApplicationModel.Resources;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// Confirmation dialog shown when the user enables a feature that can damage the
|
||||
/// hardware or otherwise leave it in a non-recoverable state. The caller supplies a
|
||||
/// resource key prefix; the dialog loads
|
||||
/// "{prefix}_WarningTitle/Header/Description/WarningList_Item{N}/Confirm".
|
||||
/// Bullets are prepended in code so translators only see the body text; the
|
||||
/// item loop probes <c>_WarningList_Item1</c>, <c>_Item2</c>, ... until a missing
|
||||
/// key is hit, so adding a 4th bullet only requires a new resw entry.
|
||||
/// </summary>
|
||||
public sealed partial class DangerousFeatureWarningDialog : ContentDialog
|
||||
{
|
||||
// Visual decorations are applied in code so translators only see body text.
|
||||
private const string WarningHeaderPrefix = "⚠️ ";
|
||||
private const string BulletPrefix = "• ";
|
||||
|
||||
// Hard cap on bullet probes; a real dialog never approaches this.
|
||||
private const int MaxBulletItems = 10;
|
||||
|
||||
// Direct ResourceMap handle so the bullet loop can probe for missing keys with
|
||||
// TryGetValue (returns null) instead of ResourceLoader.GetString (throws
|
||||
// "NamedResource Not Found").
|
||||
private static readonly ResourceMap ResourceMap =
|
||||
new ResourceManager("PowerToys.Settings.pri").MainResourceMap.GetSubtree("Resources");
|
||||
|
||||
public DangerousFeatureWarningDialog(string resourceKeyPrefix)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
var loader = ResourceLoaderInstance.ResourceLoader;
|
||||
Title = loader.GetString($"{resourceKeyPrefix}_WarningTitle");
|
||||
WarningHeader.Text = WarningHeaderPrefix + loader.GetString($"{resourceKeyPrefix}_WarningHeader");
|
||||
WarningDescription.Text = loader.GetString($"{resourceKeyPrefix}_WarningDescription");
|
||||
WarningConfirm.Text = loader.GetString($"{resourceKeyPrefix}_WarningConfirm");
|
||||
PrimaryButtonText = loader.GetString("PowerDisplay_Dialog_Enable");
|
||||
CloseButtonText = loader.GetString("PowerDisplay_Dialog_Cancel");
|
||||
|
||||
var items = new List<string>();
|
||||
for (int i = 1; i <= MaxBulletItems; i++)
|
||||
{
|
||||
var candidate = ResourceMap.TryGetValue($"{resourceKeyPrefix}_WarningList_Item{i}");
|
||||
if (candidate == null || string.IsNullOrEmpty(candidate.ValueAsString))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
items.Add(BulletPrefix + candidate.ValueAsString);
|
||||
}
|
||||
|
||||
WarningList.ItemsSource = items;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,9 +35,9 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
Loaded += (s, e) => ViewModel.OnPageLoaded();
|
||||
}
|
||||
|
||||
private async Task<bool> ShowDangerousFeatureDialogAsync(string resourceKeyPrefix)
|
||||
private async Task<bool> ShowDangerousFeatureDialogAsync(PowerDisplayWarningKind kind)
|
||||
{
|
||||
var dialog = new DangerousFeatureWarningDialog(resourceKeyPrefix) { XamlRoot = XamlRoot };
|
||||
var dialog = new PowerDisplayWarningDialog(kind) { XamlRoot = XamlRoot };
|
||||
return await dialog.ShowAsync() == ContentDialogResult.Primary;
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
cb.IsChecked == true,
|
||||
monitor.EnableColorTemperature,
|
||||
v => monitor.EnableColorTemperature = v,
|
||||
"PowerDisplay_ColorTemperature");
|
||||
PowerDisplayWarningKind.ColorTemperature);
|
||||
}
|
||||
|
||||
private async void EnablePowerState_Click(object sender, RoutedEventArgs e)
|
||||
@@ -243,7 +243,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
cb.IsChecked == true,
|
||||
monitor.EnablePowerState,
|
||||
v => monitor.EnablePowerState = v,
|
||||
"PowerDisplay_PowerState");
|
||||
PowerDisplayWarningKind.PowerState);
|
||||
}
|
||||
|
||||
private async void EnableInputSource_Click(object sender, RoutedEventArgs e)
|
||||
@@ -258,7 +258,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
cb.IsChecked == true,
|
||||
monitor.EnableInputSource,
|
||||
v => monitor.EnableInputSource = v,
|
||||
"PowerDisplay_InputSource");
|
||||
PowerDisplayWarningKind.InputSource);
|
||||
}
|
||||
|
||||
// Per-monitor CheckBoxes use OneWay binding + Click (Click only fires for real user
|
||||
@@ -270,7 +270,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
bool desiredValue,
|
||||
bool currentValue,
|
||||
Action<bool> commit,
|
||||
string resourceKeyPrefix)
|
||||
PowerDisplayWarningKind kind)
|
||||
{
|
||||
if (desiredValue == currentValue)
|
||||
{
|
||||
@@ -283,7 +283,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
return true;
|
||||
}
|
||||
|
||||
if (await ShowDangerousFeatureDialogAsync(resourceKeyPrefix))
|
||||
if (await ShowDangerousFeatureDialogAsync(kind))
|
||||
{
|
||||
commit(true);
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<ContentDialog
|
||||
x:Class="Microsoft.PowerToys.Settings.UI.Views.PowerDisplayWarningDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Microsoft.UI.Xaml.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
DefaultButton="Close"
|
||||
Style="{StaticResource DefaultContentDialogStyle}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<StackPanel Spacing="12">
|
||||
<controls:InfoBar
|
||||
x:Name="WarningInfoBar"
|
||||
IsClosable="False"
|
||||
IsOpen="True"
|
||||
Severity="Warning" />
|
||||
<TextBlock x:Name="WarningBody" TextWrapping="Wrap" />
|
||||
<HyperlinkButton x:Name="LearnMoreLink" Margin="-12,0,0,0" />
|
||||
</StackPanel>
|
||||
</ContentDialog>
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.ViewModels;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// Shared confirmation dialog for every Power Display action that can damage hardware
|
||||
/// or leave a monitor in a non-recoverable state. The caller picks a
|
||||
/// <see cref="PowerDisplayWarningKind"/> and the dialog renders a warning InfoBar
|
||||
/// (title from <c>PowerDisplay_Warning_{Kind}_InfoBar</c>), a body paragraph with
|
||||
/// bullets (from <c>PowerDisplay_Warning_{Kind}_Body</c>), and a shared
|
||||
/// title / learn-more hyperlink / Enable + Cancel buttons.
|
||||
/// </summary>
|
||||
public sealed partial class PowerDisplayWarningDialog : ContentDialog
|
||||
{
|
||||
// Shared across every variant; not localized.
|
||||
private const string LearnMoreUrl = "https://aka.ms/powerToysOverview_PowerDisplay_Note";
|
||||
|
||||
public PowerDisplayWarningDialog(PowerDisplayWarningKind kind)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
var loader = ResourceLoaderInstance.ResourceLoader;
|
||||
|
||||
// Shared chrome: same title, hyperlink, and buttons on every variant.
|
||||
Title = loader.GetString("PowerDisplay_Warning_Title");
|
||||
PrimaryButtonText = loader.GetString("PowerDisplay_Dialog_Enable");
|
||||
CloseButtonText = loader.GetString("PowerDisplay_Dialog_Cancel");
|
||||
LearnMoreLink.Content = loader.GetString("PowerDisplay_Warning_LearnMore");
|
||||
LearnMoreLink.NavigateUri = new Uri(LearnMoreUrl);
|
||||
|
||||
// Variant-specific content. The resw key pair is derived from the enum name so
|
||||
// adding a new warning is one enum value + two resw entries — no code change here.
|
||||
var prefix = $"PowerDisplay_Warning_{kind}";
|
||||
WarningInfoBar.Title = loader.GetString($"{prefix}_InfoBar");
|
||||
WarningBody.Text = loader.GetString($"{prefix}_Body");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -263,9 +263,6 @@
|
||||
AutomationProperties.AutomationId="WindowingAndLayoutsNavItem"
|
||||
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/WindowingAndLayouts.png}"
|
||||
SelectsOnInvoked="False">
|
||||
<NavigationViewItem.InfoBadge>
|
||||
<InfoBadge Style="{StaticResource NewInfoBadge}" />
|
||||
</NavigationViewItem.InfoBadge>
|
||||
<NavigationViewItem.MenuItems>
|
||||
<NavigationViewItem
|
||||
x:Name="AlwaysOnTopNavigationItem"
|
||||
@@ -290,11 +287,7 @@
|
||||
x:Uid="Shell_GrabAndMove"
|
||||
helpers:NavHelper.NavigateTo="views:GrabAndMovePage"
|
||||
AutomationProperties.AutomationId="GrabAndMoveNavItem"
|
||||
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/GrabAndMove.png}">
|
||||
<NavigationViewItem.InfoBadge>
|
||||
<InfoBadge Style="{StaticResource NewInfoBadge}" />
|
||||
</NavigationViewItem.InfoBadge>
|
||||
</NavigationViewItem>
|
||||
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/GrabAndMove.png}" />
|
||||
<NavigationViewItem
|
||||
x:Name="WorkspacesNavigationItem"
|
||||
x:Uid="Shell_Workspaces"
|
||||
@@ -311,9 +304,6 @@
|
||||
AutomationProperties.AutomationId="InputOutputNavItem"
|
||||
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/InputOutput.png}"
|
||||
SelectsOnInvoked="False">
|
||||
<NavigationViewItem.InfoBadge>
|
||||
<InfoBadge Style="{StaticResource NewInfoBadge}" />
|
||||
</NavigationViewItem.InfoBadge>
|
||||
<NavigationViewItem.MenuItems>
|
||||
<NavigationViewItem
|
||||
x:Name="KeyboardManagerNavigationItem"
|
||||
@@ -338,11 +328,7 @@
|
||||
x:Uid="Shell_PowerDisplay"
|
||||
helpers:NavHelper.NavigateTo="views:PowerDisplayPage"
|
||||
AutomationProperties.AutomationId="PowerDisplayNavItem"
|
||||
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/PowerDisplay.png}">
|
||||
<NavigationViewItem.InfoBadge>
|
||||
<InfoBadge Style="{StaticResource NewInfoBadge}" />
|
||||
</NavigationViewItem.InfoBadge>
|
||||
</NavigationViewItem>
|
||||
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/PowerDisplay.png}" />
|
||||
<NavigationViewItem
|
||||
x:Name="QuickAccentNavigationItem"
|
||||
x:Uid="Shell_QuickAccent"
|
||||
|
||||
@@ -507,7 +507,7 @@ opera.exe</value>
|
||||
<comment>Product name: Navigation view item name for PowerRename</comment>
|
||||
</data>
|
||||
<data name="Shell_ShortcutGuide.Content" xml:space="preserve">
|
||||
<value>Shortcut Guide V2</value>
|
||||
<value>Shortcut Guide</value>
|
||||
<comment>Product name: Navigation view item name for Shortcut Guide</comment>
|
||||
</data>
|
||||
<data name="Shell_PowerPreview.Content" xml:space="preserve">
|
||||
@@ -1129,7 +1129,7 @@ opera.exe</value>
|
||||
<value>Inactive color</value>
|
||||
</data>
|
||||
<data name="ShortcutGuide.ModuleDescription" xml:space="preserve">
|
||||
<value>Displays an overlay with keyboard shortcuts for Windows and your apps.</value>
|
||||
<value>Shows an on-screen overlay of keyboard shortcuts for Windows and the active application.</value>
|
||||
</data>
|
||||
<data name="ShortcutGuide_ActivationMethod.Header" xml:space="preserve">
|
||||
<value>Activation method</value>
|
||||
@@ -1375,7 +1375,7 @@ opera.exe</value>
|
||||
<comment>do not loc the product name</comment>
|
||||
</data>
|
||||
<data name="ShortcutGuide.ModuleTitle" xml:space="preserve">
|
||||
<value>Shortcut Guide V2</value>
|
||||
<value>Shortcut Guide</value>
|
||||
</data>
|
||||
<data name="General_Repository.Text" xml:space="preserve">
|
||||
<value>GitHub repository</value>
|
||||
@@ -2051,13 +2051,7 @@ Made with 💗 by Microsoft and the PowerToys community.</value>
|
||||
<value>Screen Ruler is a quick and easy way to measure pixels on your screen.</value>
|
||||
</data>
|
||||
<data name="Oobe_ShortcutGuide.Description" xml:space="preserve">
|
||||
<value>The new Shortcut Guide displays keyboard shortcuts for your apps and for the Windows environment. The following apps are included by default (with more to come in the future):
|
||||
The Microsoft Windows Operating System
|
||||
Windows Explorer
|
||||
Notepad
|
||||
Microsoft PowerToys
|
||||
|
||||
The shortcuts always correspond to the most current application/Windows version.</value>
|
||||
<value>Shortcut Guide displays keyboard shortcuts for Windows and various applications. The shortcuts shown always correspond to the most current application version or Windows version.</value>
|
||||
</data>
|
||||
<data name="Oobe_MouseUtils.Description" xml:space="preserve">
|
||||
<value>A collection of utilities to enhance your mouse.</value>
|
||||
@@ -2069,7 +2063,7 @@ The shortcuts always correspond to the most current application/Windows version.
|
||||
Take a look around and explore how some of the PowerToys utilities work and can improve your Windows experience..</value>
|
||||
</data>
|
||||
<data name="Oobe_Overview_DescriptionLinkText.Text" xml:space="preserve">
|
||||
<value>Documentation on Microsoft Learn</value>
|
||||
<value>Documentation</value>
|
||||
</data>
|
||||
<data name="Oobe_Overview_ConfigurePowerToysButton.AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Configure PowerToys</value>
|
||||
@@ -2219,7 +2213,7 @@ From there, simply click on one of the supported files in the File Explorer and
|
||||
<comment>Do not localize this string</comment>
|
||||
</data>
|
||||
<data name="Oobe_ShortcutGuide.Title" xml:space="preserve">
|
||||
<value>Shortcut Guide V2</value>
|
||||
<value>Shortcut Guide</value>
|
||||
<comment>Do not localize this string</comment>
|
||||
</data>
|
||||
<data name="Oobe_Overview_Title.Text" xml:space="preserve">
|
||||
@@ -5575,23 +5569,24 @@ The break timer font matches the text font.</value>
|
||||
<data name="PowerDisplay_Monitor_HideMonitor.Content" xml:space="preserve">
|
||||
<value>Hide monitor</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_ColorTemperature_WarningTitle" xml:space="preserve">
|
||||
<value>Confirm Color Temperature Change</value>
|
||||
<data name="PowerDisplay_Warning_Title" xml:space="preserve">
|
||||
<value>Before you continue</value>
|
||||
<comment>Title shown on every Power Display warning ContentDialog (enable module, color temperature, power state, input source, max compatibility).</comment>
|
||||
</data>
|
||||
<data name="PowerDisplay_ColorTemperature_WarningHeader" xml:space="preserve">
|
||||
<value>Warning: This is a potentially dangerous operation!</value>
|
||||
<data name="PowerDisplay_Warning_LearnMore" xml:space="preserve">
|
||||
<value>Learn more about Power Display</value>
|
||||
<comment>Text for the learn-more hyperlink shown at the bottom of every Power Display warning ContentDialog.</comment>
|
||||
</data>
|
||||
<data name="PowerDisplay_ColorTemperature_WarningDescription" xml:space="preserve">
|
||||
<value>Changing the color temperature setting may cause unpredictable results including:</value>
|
||||
<data name="PowerDisplay_Warning_ColorTemperature_InfoBar" xml:space="preserve">
|
||||
<value>Color temperature changes may not be reversible on some monitors</value>
|
||||
<comment>Title shown in the Warning InfoBar at the top of the color temperature confirmation dialog.</comment>
|
||||
</data>
|
||||
<data name="PowerDisplay_ColorTemperature_WarningList_Item1" xml:space="preserve">
|
||||
<value>Incorrect display colors or other display malfunction</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_ColorTemperature_WarningList_Item2" xml:space="preserve">
|
||||
<value>Settings that cannot be reverted</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_ColorTemperature_WarningConfirm" xml:space="preserve">
|
||||
<value>Do you want to enable color temperature control for this monitor?</value>
|
||||
<data name="PowerDisplay_Warning_ColorTemperature_Body" xml:space="preserve">
|
||||
<value>Adjusting color temperature uses DDC/CI to push values your monitor may not fully support.
|
||||
|
||||
• Some monitors display incorrect colors or behave unpredictably.
|
||||
• A small number of monitors retain the new value even after Power Display is closed and may need a factory reset to restore the original profile.</value>
|
||||
<comment>Body paragraph (with bullets) shown beneath the InfoBar in the color temperature confirmation dialog.</comment>
|
||||
</data>
|
||||
<data name="PowerDisplay_CrashDetected_IgnoreButton.Content" xml:space="preserve">
|
||||
<value>Ignore</value>
|
||||
@@ -5605,83 +5600,51 @@ The break timer font matches the text font.</value>
|
||||
<value>PowerDisplay was automatically disabled</value>
|
||||
<comment>Title of the crash recovery InfoBar shown on PowerDisplay settings page after a detected crash.</comment>
|
||||
</data>
|
||||
<data name="PowerDisplay_PowerState_WarningTitle" xml:space="preserve">
|
||||
<value>Confirm power state control</value>
|
||||
<data name="PowerDisplay_Warning_PowerState_InfoBar" xml:space="preserve">
|
||||
<value>Some monitors may need a physical power-cycle to recover</value>
|
||||
<comment>Title shown in the Warning InfoBar at the top of the power state control confirmation dialog.</comment>
|
||||
</data>
|
||||
<data name="PowerDisplay_PowerState_WarningHeader" xml:space="preserve">
|
||||
<value>Warning: This action may be unsafe.</value>
|
||||
<data name="PowerDisplay_Warning_PowerState_Body" xml:space="preserve">
|
||||
<value>Power state control uses DDC/CI to put the monitor into standby or wake it back up.
|
||||
|
||||
• Some monitors enter standby but don't wake back up from software — you may need to press the physical power button or reseat the cable.
|
||||
• Other monitors don't restore the previous state correctly when toggled.</value>
|
||||
<comment>Body paragraph (with bullets) shown beneath the InfoBar in the power state control confirmation dialog.</comment>
|
||||
</data>
|
||||
<data name="PowerDisplay_PowerState_WarningDescription" xml:space="preserve">
|
||||
<value>Enabling power state control may lead to unexpected behavior, including:</value>
|
||||
<data name="PowerDisplay_Warning_InputSource_InfoBar" xml:space="preserve">
|
||||
<value>You may lose software control until you switch back physically</value>
|
||||
<comment>Title shown in the Warning InfoBar at the top of the input source control confirmation dialog.</comment>
|
||||
</data>
|
||||
<data name="PowerDisplay_PowerState_WarningList_Item1" xml:space="preserve">
|
||||
<value>Monitor may enter standby and not wake via software — recovery may require the physical power button or reseating the cable.</value>
|
||||
<data name="PowerDisplay_Warning_InputSource_Body" xml:space="preserve">
|
||||
<value>Input source control switches the monitor to a different input via DDC/CI.
|
||||
|
||||
• If the selected input has no signal, the screen will go black until something is connected.
|
||||
• After switching, software control may be unavailable until you change inputs again using the monitor's physical buttons.
|
||||
• Not every monitor reliably exposes all of its inputs over DDC/CI.</value>
|
||||
<comment>Body paragraph (with bullets) shown beneath the InfoBar in the input source control confirmation dialog.</comment>
|
||||
</data>
|
||||
<data name="PowerDisplay_PowerState_WarningList_Item2" xml:space="preserve">
|
||||
<value>Some monitors may not restore the previous state correctly.</value>
|
||||
<data name="PowerDisplay_Warning_MaxCompatibility_InfoBar" xml:space="preserve">
|
||||
<value>Probing every VCP feature can destabilize some monitors</value>
|
||||
<comment>Title shown in the Warning InfoBar at the top of the maximum compatibility mode confirmation dialog.</comment>
|
||||
</data>
|
||||
<data name="PowerDisplay_PowerState_WarningConfirm" xml:space="preserve">
|
||||
<value>Enable power state control for this monitor?</value>
|
||||
<data name="PowerDisplay_Warning_MaxCompatibility_Body" xml:space="preserve">
|
||||
<value>Maximum compatibility mode ignores your monitor's capabilities string and probes every supported VCP feature directly. Only enable this if a monitor is missing from Power Display.
|
||||
|
||||
• The screen may briefly go black or lose signal during discovery.
|
||||
• The monitor may respond unpredictably — including unwanted changes to brightness, contrast, or input source.
|
||||
• The monitor may stop responding to DDC/CI altogether until it's power-cycled.</value>
|
||||
<comment>Body paragraph (with bullets) shown beneath the InfoBar in the maximum compatibility mode confirmation dialog.</comment>
|
||||
</data>
|
||||
<data name="PowerDisplay_InputSource_WarningTitle" xml:space="preserve">
|
||||
<value>Confirm input source control</value>
|
||||
<data name="PowerDisplay_Warning_EnableModule_InfoBar" xml:space="preserve">
|
||||
<value>This may cause a system crash on a small number of monitors</value>
|
||||
<comment>Title shown in the Warning InfoBar at the top of the Power Display enable confirmation dialog.</comment>
|
||||
</data>
|
||||
<data name="PowerDisplay_InputSource_WarningHeader" xml:space="preserve">
|
||||
<value>Warning: This action may be unsafe.</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_InputSource_WarningDescription" xml:space="preserve">
|
||||
<value>Switching input sources may cause unintended results, including:</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_InputSource_WarningList_Item1" xml:space="preserve">
|
||||
<value>Screen may go black if the selected input has no signal.</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_InputSource_WarningList_Item2" xml:space="preserve">
|
||||
<value>Software control may be lost until you switch back using the monitor’s physical buttons.</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_InputSource_WarningList_Item3" xml:space="preserve">
|
||||
<value>Some monitors may not reliably expose all input sources.</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_InputSource_WarningConfirm" xml:space="preserve">
|
||||
<value>Enable input source control for this monitor?</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_MaxCompatibility_WarningTitle" xml:space="preserve">
|
||||
<value>Confirm maximum compatibility mode</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_MaxCompatibility_WarningHeader" xml:space="preserve">
|
||||
<value>Warning: This may cause unstable monitor behavior.</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_MaxCompatibility_WarningDescription" xml:space="preserve">
|
||||
<value>Maximum compatibility mode bypasses your monitor's capabilities string and sends a probe for every supported VCP feature. Some monitors do not handle this well, which may cause:</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_MaxCompatibility_WarningList_Item1" xml:space="preserve">
|
||||
<value>The screen may go black or briefly lose signal during discovery.</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_MaxCompatibility_WarningList_Item2" xml:space="preserve">
|
||||
<value>The monitor may respond unpredictably, including unwanted changes to brightness, contrast, or input source.</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_MaxCompatibility_WarningList_Item3" xml:space="preserve">
|
||||
<value>The monitor may stop responding to DDC/CI until it is power-cycled.</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_MaxCompatibility_WarningConfirm" xml:space="preserve">
|
||||
<value>Only enable this if a monitor is missing from Power Display and you fully understand the side effects listed above. Continue?</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_EnableModule_WarningTitle" xml:space="preserve">
|
||||
<value>Enable Power Display?</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_EnableModule_WarningHeader" xml:space="preserve">
|
||||
<value>Warning: This may cause a system crash on a small number of monitors.</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_EnableModule_WarningDescription" xml:space="preserve">
|
||||
<value>Power Display reads each connected monitor's DDC/CI capabilities at startup. On a small number of monitors with malformed capability strings, this read has been observed to trigger a kernel-side bug that causes Windows to bug-check (BSOD). The underlying issue is in Windows, not in Power Display, but the read is what surfaces it. Possible effects:</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_EnableModule_WarningList_Item1" xml:space="preserve">
|
||||
<value>On affected monitors, the system may crash and restart.</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_EnableModule_WarningList_Item2" xml:space="preserve">
|
||||
<value>If a crash is detected, Power Display will auto-disable itself on next launch; you'll need to re-enable the module and dismiss the warning on the settings page each time.</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_EnableModule_WarningConfirm" xml:space="preserve">
|
||||
<value>Only enable Power Display if you accept the risk above. Continue?</value>
|
||||
<data name="PowerDisplay_Warning_EnableModule_Body" xml:space="preserve">
|
||||
<value>Power Display reads each connected monitor's DDC/CI capabilities at startup. On a small number of monitors with malformed capability strings, this read can trigger a Windows kernel bug that causes a system crash (BSOD). The underlying issue is in Windows, not in Power Display.
|
||||
|
||||
• On affected monitors, the system may crash and restart.
|
||||
• If a crash is detected, Power Display will automatically disable itself the next time PowerToys launches, and you'll need to re-enable the module each time.</value>
|
||||
<comment>Body paragraph (with bullets) shown beneath the InfoBar in the Power Display enable confirmation dialog.</comment>
|
||||
</data>
|
||||
<data name="PowerDisplay_Dialog_Enable" xml:space="preserve">
|
||||
<value>Enable</value>
|
||||
|
||||
@@ -162,7 +162,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
try
|
||||
{
|
||||
if (await ConfirmDangerousFeatureAsync("PowerDisplay_EnableModule"))
|
||||
if (await ConfirmDangerousFeatureAsync(PowerDisplayWarningKind.EnableModule))
|
||||
{
|
||||
CommitIsEnabled(true);
|
||||
}
|
||||
@@ -243,7 +243,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
/// View-supplied confirmation dialog. Default no-op denies all dangerous enables;
|
||||
/// PowerDisplayPage replaces this in its constructor with a real dialog show.
|
||||
/// </summary>
|
||||
public Func<string, Task<bool>> ConfirmDangerousFeatureAsync { get; set; } = _ => Task.FromResult(false);
|
||||
public Func<PowerDisplayWarningKind, Task<bool>> ConfirmDangerousFeatureAsync { get; set; } = _ => Task.FromResult(false);
|
||||
|
||||
// Dangerous toggle. TwoWay-bound to the UI; the setter handles the "ask first"
|
||||
// gesture entirely inside the ViewModel. Initial-binding push and post-cancel
|
||||
@@ -278,7 +278,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
try
|
||||
{
|
||||
if (await ConfirmDangerousFeatureAsync("PowerDisplay_MaxCompatibility"))
|
||||
if (await ConfirmDangerousFeatureAsync(PowerDisplayWarningKind.MaxCompatibility))
|
||||
{
|
||||
_settings.Properties.MaxCompatibilityMode = true;
|
||||
NotifySettingsChanged();
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies which Power Display dangerous-feature confirmation dialog to show.
|
||||
/// Each value maps to a pair of resw keys
|
||||
/// (<c>PowerDisplay_Warning_{Kind}_InfoBar</c> + <c>PowerDisplay_Warning_{Kind}_Body</c>)
|
||||
/// inside the shared <c>PowerDisplayWarningDialog</c> control.
|
||||
/// </summary>
|
||||
public enum PowerDisplayWarningKind
|
||||
{
|
||||
/// <summary>Shown when the user turns on the Power Display module itself.</summary>
|
||||
EnableModule,
|
||||
|
||||
/// <summary>Shown when the user enables color temperature control for a monitor.</summary>
|
||||
ColorTemperature,
|
||||
|
||||
/// <summary>Shown when the user enables power state control for a monitor.</summary>
|
||||
PowerState,
|
||||
|
||||
/// <summary>Shown when the user enables input source control for a monitor.</summary>
|
||||
InputSource,
|
||||
|
||||
/// <summary>Shown when the user enables maximum compatibility mode.</summary>
|
||||
MaxCompatibility,
|
||||
}
|
||||
}
|
||||