Compare commits

..

1 Commits

Author SHA1 Message Date
Gordon Lam (SH)
3bd9698073 Add Parallel project build 2025-10-11 10:03:54 +08:00
33 changed files with 100 additions and 1057 deletions

View File

@@ -258,6 +258,7 @@ jobs:
-restore -graph
/p:RestorePackagesConfig=true
/p:CIBuild=true
/p:BuildInParallel=true
/bl:$(LogOutputDirectory)\build-0-main.binlog
${{ parameters.additionalBuildOptions }}
$(MSBuildCacheParameters)
@@ -277,7 +278,7 @@ jobs:
condition: ne(variables['BuildPlatform'], 'x64')
inputs:
solution: src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj
msbuildArgs: /t:Build /m /restore
msbuildArgs: /t:Build /m /restore /p:BuildInParallel=true
platform: x64
configuration: $(BuildConfiguration)
msbuildArchitecture: x64
@@ -323,6 +324,7 @@ jobs:
-restore -graph
/p:RestorePackagesConfig=true
/p:CIBuild=true
/p:BuildInParallel=true
/bl:$(LogOutputDirectory)\build-bug-report.binlog
${{ parameters.additionalBuildOptions }}
$(MSBuildCacheParameters)
@@ -344,6 +346,7 @@ jobs:
-restore -graph
/p:RestorePackagesConfig=true
/p:CIBuild=true
/p:BuildInParallel=true
/bl:$(LogOutputDirectory)\build-styles-report.binlog
${{ parameters.additionalBuildOptions }}
$(MSBuildCacheParameters)
@@ -365,7 +368,7 @@ jobs:
msbuildArgs: >-
/target:Publish
/graph
/p:Configuration=$(BuildConfiguration);Platform=$(BuildPlatform);AppxBundle=Never
/p:Configuration=$(BuildConfiguration);Platform=$(BuildPlatform);AppxBundle=Never;BuildInParallel=true
/p:VCRTForwarders-IncludeDebugCRT=false
/p:PowerToysRoot=$(Build.SourcesDirectory)
/p:PublishProfile=InstallationPublishProfile.pubxml

View File

@@ -90,6 +90,7 @@ jobs:
/p:RestorePackagesConfig=true
/p:BuildProjectReferences=true
/p:CIBuild=true
/p:BuildInParallel=true
/bl:$(LogOutputDirectory)\build-all-uitests.binlog
$(NUGET_RESTORE_MSBUILD_ARGS)
platform: $(BuildPlatform)
@@ -111,6 +112,7 @@ jobs:
/p:RestorePackagesConfig=true
/p:BuildProjectReferences=true
/p:CIBuild=true
/p:BuildInParallel=true
/bl:$(LogOutputDirectory)\build-${{ module }}.binlog
$(NUGET_RESTORE_MSBUILD_ARGS)
platform: $(BuildPlatform)

View File

@@ -1,153 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.CompilerServices;
using Windows.Win32;
using Windows.Win32.Storage.FileSystem;
namespace Microsoft.CmdPal.Core.Common.Helpers;
public static class PathHelper
{
public static bool Exists(string path, out bool isDirectory)
{
isDirectory = false;
if (string.IsNullOrEmpty(path))
{
return false;
}
string? fullPath;
try
{
fullPath = Path.GetFullPath(path);
}
catch (Exception ex) when (ex is ArgumentException or IOException or UnauthorizedAccessException)
{
return false;
}
var result = ExistsCore(fullPath, out isDirectory);
if (result && IsDirectorySeparator(fullPath[^1]))
{
// Some sys-calls remove all trailing slashes and may give false positives for existing files.
// We want to make sure that if the path ends in a trailing slash, it's truly a directory.
return isDirectory;
}
return result;
}
/// <summary>
/// Normalize potential local/UNC file path text input: trim whitespace and surrounding quotes.
/// Windows file paths cannot contain quotes, but user input can include them.
/// </summary>
public static string Unquote(string? text)
{
return string.IsNullOrWhiteSpace(text) ? (text ?? string.Empty) : text.Trim().Trim('"');
}
/// <summary>
/// Quick heuristic to determine if the string looks like a Windows file path (UNC or drive-letter based).
/// </summary>
public static bool LooksLikeFilePath(string? path)
{
if (string.IsNullOrWhiteSpace(path))
{
return false;
}
// UNC path
if (path.StartsWith(@"\\", StringComparison.Ordinal))
{
// Win32 File Namespaces \\?\
if (path.StartsWith(@"\\?\", StringComparison.Ordinal))
{
return IsSlow(path[4..]);
}
// Basic UNC path validation: \\server\share or \\server\share\path
var parts = path[2..].Split('\\', StringSplitOptions.RemoveEmptyEntries);
return parts.Length >= 2; // At minimum: server and share
}
// Drive letter path (e.g., C:\ or C:)
return path.Length >= 2 && char.IsLetter(path[0]) && path[1] == ':';
}
/// <summary>
/// Validates path syntax without performing any I/O by using Path.GetFullPath.
/// </summary>
public static bool HasValidPathSyntax(string? path)
{
if (string.IsNullOrWhiteSpace(path))
{
return false;
}
try
{
_ = Path.GetFullPath(path);
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Checks if a string represents a valid Windows file path (local or network)
/// using fast syntax validation only. Reuses LooksLikeFilePath and HasValidPathSyntax.
/// </summary>
public static bool IsValidFilePath(string? path)
{
return LooksLikeFilePath(path) && HasValidPathSyntax(path);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsDirectorySeparator(char c)
{
return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar;
}
private static bool ExistsCore(string fullPath, out bool isDirectory)
{
var attributes = PInvoke.GetFileAttributes(fullPath);
var result = attributes != PInvoke.INVALID_FILE_ATTRIBUTES;
isDirectory = result && (attributes & (uint)FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_DIRECTORY) != 0;
return result;
}
public static bool IsSlow(string path)
{
if (string.IsNullOrEmpty(path))
{
return false;
}
try
{
var root = Path.GetPathRoot(path);
if (!string.IsNullOrEmpty(root))
{
if (root.Length > 2 && char.IsLetter(root[0]) && root[1] == ':')
{
return new DriveInfo(root).DriveType is not (DriveType.Fixed or DriveType.Ram);
}
else if (root.StartsWith(@"\\", StringComparison.Ordinal))
{
return !root.StartsWith(@"\\?\", StringComparison.Ordinal) || IsSlow(root[4..]);
}
}
return false;
}
catch
{
return false;
}
}
}

View File

@@ -12,8 +12,4 @@ MonitorFromWindow
SHOW_WINDOW_CMD
ShellExecuteEx
SEE_MASK_INVOKEIDLIST
GetFileAttributes
FILE_FLAGS_AND_ATTRIBUTES
INVALID_FILE_ATTRIBUTES
SEE_MASK_INVOKEIDLIST

View File

@@ -2,10 +2,8 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using CommunityToolkit.Mvvm.Input;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Core.ViewModels;
@@ -13,13 +11,6 @@ public partial class DetailsLinkViewModel(
IDetailsElement _detailsElement,
WeakReference<IPageContext> context) : DetailsElementViewModel(_detailsElement, context)
{
private static readonly string[] _initProperties = [
nameof(Text),
nameof(Link),
nameof(IsLink),
nameof(IsText),
nameof(NavigateCommand)];
private readonly ExtensionObject<IDetailsLink> _dataModel =
new(_detailsElement.Data as IDetailsLink);
@@ -31,8 +22,6 @@ public partial class DetailsLinkViewModel(
public bool IsText => !IsLink;
public RelayCommand? NavigateCommand { get; private set; }
public override void InitializeProperties()
{
base.InitializeProperties();
@@ -49,18 +38,9 @@ public partial class DetailsLinkViewModel(
Text = Link.ToString();
}
if (Link is not null)
{
// Custom command to open a link in the default browser or app,
// depending on the link type.
// Binding Link to a Hyperlink(Button).NavigateUri works only for
// certain URI schemes (e.g., http, https) and cannot open file:
// scheme URIs or local files.
NavigateCommand = new RelayCommand(
() => ShellHelpers.OpenInShell(Link.ToString()),
() => Link is not null);
}
UpdateProperty(_initProperties);
UpdateProperty(nameof(Text));
UpdateProperty(nameof(Link));
UpdateProperty(nameof(IsLink));
UpdateProperty(nameof(IsText));
}
}

View File

@@ -265,9 +265,6 @@ public partial class ShellViewModel : ObservableObject,
throw new NotSupportedException();
}
// Clear command bar, ViewModel initialization can already set new commands if it wants to
OnUIThread(() => WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null)));
// Kick off async loading of our ViewModel
LoadPageViewModelAsync(pageViewModel, navigationToken)
.ContinueWith(
@@ -278,6 +275,9 @@ public partial class ShellViewModel : ObservableObject,
{
newCts.Dispose();
}
// When we're done loading the page, then update the command bar to match
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null));
},
navigationToken,
TaskContinuationOptions.None,

View File

@@ -244,36 +244,7 @@ public partial class MainListPage : DynamicListPage,
var commands = _tlcManager.TopLevelCommands;
lock (commands)
{
if (token.IsCancellationRequested)
{
return;
}
// prefilter fallbacks
var specialFallbacks = new List<TopLevelViewModel>(_specialFallbacks.Length);
var commonFallbacks = new List<TopLevelViewModel>();
foreach (var s in commands)
{
if (!s.IsFallback)
{
continue;
}
if (_specialFallbacks.Contains(s.CommandProviderId))
{
specialFallbacks.Add(s);
}
else
{
commonFallbacks.Add(s);
}
}
// start update of fallbacks; update special fallbacks separately,
// so they can finish faster
UpdateFallbacks(SearchText, specialFallbacks, token);
UpdateFallbacks(SearchText, commonFallbacks, token);
UpdateFallbacks(SearchText, commands.ToImmutableArray(), token);
if (token.IsCancellationRequested)
{
@@ -345,12 +316,15 @@ public partial class MainListPage : DynamicListPage,
// with a list of all our commands & apps.
if (!newFilteredItems.Any() && !newApps.Any())
{
// We're going to start over with our fallbacks
newFallbacks = Enumerable.Empty<IListItem>();
newFilteredItems = commands.Where(s => !s.IsFallback);
// Fallbacks are always included in the list, even if they
// don't match the search text. But we don't want to
// consider them when filtering the list.
newFallbacks = commonFallbacks;
newFallbacks = commands.Where(s => s.IsFallback && !_specialFallbacks.Contains(s.CommandProviderId));
if (token.IsCancellationRequested)
{

View File

@@ -78,12 +78,6 @@ public sealed partial class ContentPage : Page,
WeakReferenceMessenger.Default.Unregister<ActivateSecondaryCommandMessage>(this);
// Clean-up event listeners
if (e.NavigationMode != NavigationMode.New)
{
ViewModel?.SafeCleanup();
CleanupHelper.Cleanup(this);
}
ViewModel = null;
}

View File

@@ -108,7 +108,6 @@
Visibility="{x:Bind IsText, Mode=OneWay}" />
<HyperlinkButton
Padding="0"
Command="{x:Bind NavigateCommand, Mode=OneWay}"
NavigateUri="{x:Bind Link, Mode=OneWay}"
Visibility="{x:Bind IsLink, Mode=OneWay}">
<TextBlock Text="{x:Bind Text, Mode=OneWay}" TextWrapping="Wrap" />

View File

@@ -1,35 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
/// <summary>
/// Abstraction for providers that can extract metadata and offer actions for a clipboard context.
/// </summary>
internal interface IClipboardMetadataProvider
{
/// <summary>
/// Gets the section title to show in the UI for this provider's metadata.
/// </summary>
string SectionTitle { get; }
/// <summary>
/// Returns true if this provider can produce metadata for the given item.
/// </summary>
bool CanHandle(ClipboardItem item);
/// <summary>
/// Returns metadata elements for the UI. Caller decides section grouping.
/// </summary>
IEnumerable<DetailsElement> GetDetails(ClipboardItem item);
/// <summary>
/// Returns context actions to be appended to MoreCommands. Use unique IDs for de-duplication.
/// </summary>
IEnumerable<ProviderAction> GetActions(ClipboardItem item);
}

View File

@@ -1,12 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
internal sealed record ImageMetadata(
uint Width,
uint Height,
double DpiX,
double DpiY,
ulong? StorageSize);

View File

@@ -1,55 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading.Tasks;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
internal static class ImageMetadataAnalyzer
{
/// <summary>
/// Reads image metadata from a RandomAccessStreamReference without decoding pixels.
/// Returns oriented dimensions (EXIF rotation applied).
/// </summary>
public static async Task<ImageMetadata> GetAsync(RandomAccessStreamReference reference)
{
ArgumentNullException.ThrowIfNull(reference);
using IRandomAccessStream ras = await reference.OpenReadAsync().AsTask().ConfigureAwait(false);
var sizeBytes = TryGetSize(ras);
// BitmapDecoder does not decode pixel data unless you ask it to,
// so this is fast and memory-friendly.
var decoder = await BitmapDecoder.CreateAsync(ras).AsTask().ConfigureAwait(false);
// OrientedPixelWidth/Height account for EXIF orientation
var width = decoder.OrientedPixelWidth;
var height = decoder.OrientedPixelHeight;
return new ImageMetadata(
Width: width,
Height: height,
DpiX: decoder.DpiX,
DpiY: decoder.DpiY,
StorageSize: sizeBytes);
}
private static ulong? TryGetSize(IRandomAccessStream s)
{
try
{
// On file-backed streams this is accurate.
// On some URI/virtual streams this may be unsupported or 0.
var size = s.Size;
return size == 0 ? (ulong?)0 : size;
}
catch
{
return null;
}
}
}

View File

@@ -1,60 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using ManagedCommon;
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
internal sealed class ImageMetadataProvider : IClipboardMetadataProvider
{
public string SectionTitle => "Image metadata";
public bool CanHandle(ClipboardItem item) => item.IsImage;
public IEnumerable<DetailsElement> GetDetails(ClipboardItem item)
{
var result = new List<DetailsElement>();
if (!CanHandle(item) || item.ImageData is null)
{
return result;
}
try
{
var metadata = ImageMetadataAnalyzer.GetAsync(item.ImageData).GetAwaiter().GetResult();
result.Add(new DetailsElement
{
Key = "Dimensions",
Data = new DetailsLink($"{metadata.Width} x {metadata.Height}"),
});
result.Add(new DetailsElement
{
Key = "DPI",
Data = new DetailsLink($"{metadata.DpiX:0.###} x {metadata.DpiY:0.###}"),
});
if (metadata.StorageSize != null)
{
result.Add(new DetailsElement
{
Key = "Storage size",
Data = new DetailsLink(SizeFormatter.FormatSize(metadata.StorageSize.Value)),
});
}
}
catch (Exception ex)
{
Logger.LogDebug("Failed to retrieve image metadata:" + ex);
}
return result;
}
public IEnumerable<ProviderAction> GetActions(ClipboardItem item) => [];
}

View File

@@ -1,14 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
internal enum LineEndingType
{
None,
Windows, // \r\n (CRLF)
Unix, // \n (LF)
Mac, // \r (CR)
Mixed,
}

View File

@@ -1,14 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
/// <summary>
/// Represents an action exposed by a metadata provider.
/// </summary>
/// <param name="Id">Unique identifier for de-duplication (case-insensitive).</param>
/// <param name="Action">The actual context menu item to be shown.</param>
internal readonly record struct ProviderAction(string Id, CommandContextItem Action);

View File

@@ -1,49 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Globalization;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
/// <summary>
/// Utility for formatting byte sizes to a human-readable string.
/// </summary>
internal static class SizeFormatter
{
private const long KB = 1024;
private const long MB = 1024 * KB;
private const long GB = 1024 * MB;
public static string FormatSize(long bytes)
{
return bytes switch
{
>= GB => string.Format(CultureInfo.CurrentCulture, "{0:F2} GB", (double)bytes / GB),
>= MB => string.Format(CultureInfo.CurrentCulture, "{0:F2} MB", (double)bytes / MB),
>= KB => string.Format(CultureInfo.CurrentCulture, "{0:F2} KB", (double)bytes / KB),
_ => string.Format(CultureInfo.CurrentCulture, "{0} B", bytes),
};
}
public static string FormatSize(ulong bytes)
{
// Use double for division to avoid overflow; thresholds mirror long version
if (bytes >= (ulong)GB)
{
return string.Format(CultureInfo.CurrentCulture, "{0:F2} GB", bytes / (double)GB);
}
if (bytes >= (ulong)MB)
{
return string.Format(CultureInfo.CurrentCulture, "{0:F2} MB", bytes / (double)MB);
}
if (bytes >= (ulong)KB)
{
return string.Format(CultureInfo.CurrentCulture, "{0:F2} KB", bytes / (double)KB);
}
return string.Format(CultureInfo.CurrentCulture, "{0} B", bytes);
}
}

View File

@@ -1,138 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using ManagedCommon;
using Microsoft.CmdPal.Core.Common.Helpers;
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
/// <summary>
/// Detects when text content is a valid existing file or directory path and exposes basic metadata.
/// </summary>
internal sealed class TextFileSystemMetadataProvider : IClipboardMetadataProvider
{
public string SectionTitle => "File";
public bool CanHandle(ClipboardItem item)
{
ArgumentNullException.ThrowIfNull(item);
if (!item.IsText || string.IsNullOrWhiteSpace(item.Content))
{
return false;
}
var text = PathHelper.Unquote(item.Content);
return PathHelper.IsValidFilePath(text);
}
public IEnumerable<DetailsElement> GetDetails(ClipboardItem item)
{
ArgumentNullException.ThrowIfNull(item);
var result = new List<DetailsElement>();
if (!item.IsText || string.IsNullOrWhiteSpace(item.Content))
{
return result;
}
var path = PathHelper.Unquote(item.Content);
if (PathHelper.IsSlow(path) || !PathHelper.Exists(path, out var isDirectory))
{
result.Add(new DetailsElement { Key = "Name", Data = new DetailsLink(Path.GetFileName(path)) });
result.Add(new DetailsElement { Key = "Location", Data = new DetailsLink(UrlHelper.NormalizeUrl(path), path) });
return result;
}
try
{
if (!isDirectory)
{
var fi = new FileInfo(path);
result.Add(new DetailsElement { Key = "Name", Data = new DetailsLink(fi.Name) });
result.Add(new DetailsElement { Key = "Location", Data = new DetailsLink(UrlHelper.NormalizeUrl(fi.FullName), fi.FullName) });
result.Add(new DetailsElement { Key = "Type", Data = new DetailsLink(fi.Extension) });
result.Add(new DetailsElement { Key = "Size", Data = new DetailsLink(SizeFormatter.FormatSize(fi.Length)) });
result.Add(new DetailsElement { Key = "Modified", Data = new DetailsLink(fi.LastWriteTime.ToString(CultureInfo.CurrentCulture)) });
result.Add(new DetailsElement { Key = "Created", Data = new DetailsLink(fi.CreationTime.ToString(CultureInfo.CurrentCulture)) });
}
else
{
var di = new DirectoryInfo(path);
result.Add(new DetailsElement { Key = "Name", Data = new DetailsLink(di.Name) });
result.Add(new DetailsElement { Key = "Location", Data = new DetailsLink(UrlHelper.NormalizeUrl(di.FullName), di.FullName) });
result.Add(new DetailsElement { Key = "Type", Data = new DetailsLink("Folder") });
result.Add(new DetailsElement { Key = "Modified", Data = new DetailsLink(di.LastWriteTime.ToString(CultureInfo.CurrentCulture)) });
result.Add(new DetailsElement { Key = "Created", Data = new DetailsLink(di.CreationTime.ToString(CultureInfo.CurrentCulture)) });
}
}
catch (Exception ex)
{
Logger.LogError("Failed to retrieve file system metadata.", ex);
}
return result;
}
public IEnumerable<ProviderAction> GetActions(ClipboardItem item)
{
ArgumentNullException.ThrowIfNull(item);
if (!item.IsText || string.IsNullOrWhiteSpace(item.Content))
{
yield break;
}
var path = PathHelper.Unquote(item.Content);
if (PathHelper.IsSlow(path) || !PathHelper.Exists(path, out var isDirectory))
{
// One anything
var open = new CommandContextItem(new OpenFileCommand(path)) { RequestedShortcut = KeyChords.OpenUrl };
yield return new ProviderAction(WellKnownActionIds.Open, open);
yield break;
}
if (!isDirectory)
{
// Open file
var open = new CommandContextItem(new OpenFileCommand(path)) { RequestedShortcut = KeyChords.OpenUrl };
yield return new ProviderAction(WellKnownActionIds.Open, open);
// Show in folder (select)
var show = new CommandContextItem(new ShowFileInFolderCommand(path)) { RequestedShortcut = WellKnownKeyChords.OpenFileLocation };
yield return new ProviderAction(WellKnownActionIds.OpenLocation, show);
// Copy path
var copy = new CommandContextItem(new CopyPathCommand(path)) { RequestedShortcut = WellKnownKeyChords.CopyFilePath };
yield return new ProviderAction(WellKnownActionIds.CopyPath, copy);
// Open in console at file location
var openConsole = new CommandContextItem(OpenInConsoleCommand.FromFile(path)) { RequestedShortcut = WellKnownKeyChords.OpenInConsole };
yield return new ProviderAction(WellKnownActionIds.OpenConsole, openConsole);
}
else
{
// Open folder
var openFolder = new CommandContextItem(new OpenFileCommand(path)) { RequestedShortcut = KeyChords.OpenUrl };
yield return new ProviderAction(WellKnownActionIds.Open, openFolder);
// Open in console
var openConsole = new CommandContextItem(OpenInConsoleCommand.FromDirectory(path)) { RequestedShortcut = WellKnownKeyChords.OpenInConsole };
yield return new ProviderAction(WellKnownActionIds.OpenConsole, openConsole);
// Copy path
var copy = new CommandContextItem(new CopyPathCommand(path)) { RequestedShortcut = WellKnownKeyChords.CopyFilePath };
yield return new ProviderAction(WellKnownActionIds.CopyPath, copy);
}
}
}

View File

@@ -1,25 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
internal sealed record TextMetadata
{
public int CharacterCount { get; init; }
public int WordCount { get; init; }
public int SentenceCount { get; init; }
public int LineCount { get; init; }
public int ParagraphCount { get; init; }
public LineEndingType LineEnding { get; init; }
public override string ToString()
{
return $"Characters: {CharacterCount}, Words: {WordCount}, Sentences: {SentenceCount}, Lines: {LineCount}, Paragraphs: {ParagraphCount}, Line Ending: {LineEnding}";
}
}

View File

@@ -1,109 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Linq;
using System.Text.RegularExpressions;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
internal partial class TextMetadataAnalyzer
{
public TextMetadata Analyze(string input)
{
ArgumentNullException.ThrowIfNull(input);
return new TextMetadata
{
CharacterCount = input.Length,
WordCount = CountWords(input),
SentenceCount = CountSentences(input),
LineCount = CountLines(input),
ParagraphCount = CountParagraphs(input),
LineEnding = DetectLineEnding(input),
};
}
private LineEndingType DetectLineEnding(string text)
{
var crlfCount = Regex.Matches(text, "\r\n").Count;
var lfCount = Regex.Matches(text, "(?<!\r)\n").Count;
var crCount = Regex.Matches(text, "\r(?!\n)").Count;
var endingTypes = (crlfCount > 0 ? 1 : 0) + (lfCount > 0 ? 1 : 0) + (crCount > 0 ? 1 : 0);
if (endingTypes > 1)
{
return LineEndingType.Mixed;
}
if (crlfCount > 0)
{
return LineEndingType.Windows;
}
if (lfCount > 0)
{
return LineEndingType.Unix;
}
if (crCount > 0)
{
return LineEndingType.Mac;
}
return LineEndingType.None;
}
private int CountLines(string text)
{
if (string.IsNullOrEmpty(text))
{
return 0;
}
return text.Count(c => c == '\n') + 1;
}
private int CountParagraphs(string text)
{
if (string.IsNullOrEmpty(text))
{
return 0;
}
var paragraphs = ParagraphsRegex()
.Split(text)
.Count(static p => !string.IsNullOrWhiteSpace(p));
return paragraphs > 0 ? paragraphs : 1;
}
private int CountWords(string text)
{
if (string.IsNullOrEmpty(text))
{
return 0;
}
return Regex.Matches(text, @"\b\w+\b").Count;
}
private int CountSentences(string text)
{
if (string.IsNullOrEmpty(text))
{
return 0;
}
var matches = SentencesRegex().Matches(text);
return matches.Count > 0 ? matches.Count : (text.Trim().Length > 0 ? 1 : 0);
}
[GeneratedRegex(@"(\r?\n){2,}")]
private static partial Regex ParagraphsRegex();
[GeneratedRegex(@"[.!?]+(?=\s|$)")]
private static partial Regex SentencesRegex();
}

View File

@@ -1,63 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Globalization;
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
internal sealed class TextMetadataProvider : IClipboardMetadataProvider
{
public string SectionTitle => "Text statistics";
public bool CanHandle(ClipboardItem item) => item.IsText;
public IEnumerable<DetailsElement> GetDetails(ClipboardItem item)
{
var result = new List<DetailsElement>();
if (!CanHandle(item) || string.IsNullOrEmpty(item.Content))
{
return result;
}
var r = new TextMetadataAnalyzer().Analyze(item.Content);
result.Add(new DetailsElement
{
Key = "Characters",
Data = new DetailsLink(r.CharacterCount.ToString(CultureInfo.CurrentCulture)),
});
result.Add(new DetailsElement
{
Key = "Words",
Data = new DetailsLink(r.WordCount.ToString(CultureInfo.CurrentCulture)),
});
result.Add(new DetailsElement
{
Key = "Sentences",
Data = new DetailsLink(r.SentenceCount.ToString(CultureInfo.CurrentCulture)),
});
result.Add(new DetailsElement
{
Key = "Lines",
Data = new DetailsLink(r.LineCount.ToString(CultureInfo.CurrentCulture)),
});
result.Add(new DetailsElement
{
Key = "Paragraphs",
Data = new DetailsLink(r.ParagraphCount.ToString(CultureInfo.CurrentCulture)),
});
result.Add(new DetailsElement
{
Key = "Line Ending",
Data = new DetailsLink(r.LineEnding.ToString()),
});
return result;
}
public IEnumerable<ProviderAction> GetActions(ClipboardItem item) => [];
}

View File

@@ -1,113 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
/// <summary>
/// Detects web links in text and shows normalized URL and key parts.
/// </summary>
internal sealed class WebLinkMetadataProvider : IClipboardMetadataProvider
{
public string SectionTitle => "Link";
public bool CanHandle(ClipboardItem item)
{
if (!item.IsText || string.IsNullOrWhiteSpace(item.Content))
{
return false;
}
if (!UrlHelper.IsValidUrl(item.Content))
{
return false;
}
var normalized = UrlHelper.NormalizeUrl(item.Content);
if (!Uri.TryCreate(normalized, UriKind.Absolute, out var uri))
{
return false;
}
// Exclude file: scheme; it's handled by TextFileSystemMetadataProvider
return !uri.Scheme.Equals(Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase);
}
public IEnumerable<DetailsElement> GetDetails(ClipboardItem item)
{
var result = new List<DetailsElement>();
if (!item.IsText || string.IsNullOrWhiteSpace(item.Content))
{
return result;
}
try
{
var normalized = UrlHelper.NormalizeUrl(item.Content);
if (!Uri.TryCreate(normalized, UriKind.Absolute, out var uri))
{
return result;
}
// Skip file: at runtime as well (defensive)
if (uri.Scheme.Equals(Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase))
{
return result;
}
result.Add(new DetailsElement { Key = "URL", Data = new DetailsLink(normalized) });
result.Add(new DetailsElement { Key = "Host", Data = new DetailsLink(uri.Host) });
if (!uri.IsDefaultPort)
{
result.Add(new DetailsElement { Key = "Port", Data = new DetailsLink(uri.Port.ToString(CultureInfo.CurrentCulture)) });
}
if (!string.IsNullOrEmpty(uri.AbsolutePath) && uri.AbsolutePath != "/")
{
result.Add(new DetailsElement { Key = "Path", Data = new DetailsLink(uri.AbsolutePath) });
}
if (!string.IsNullOrEmpty(uri.Query))
{
var q = uri.Query;
var count = q.Count(static c => c == '&') + (q.Length > 1 ? 1 : 0);
result.Add(new DetailsElement { Key = "Query params", Data = new DetailsLink(count.ToString(CultureInfo.CurrentCulture)) });
}
if (!string.IsNullOrEmpty(uri.Fragment))
{
result.Add(new DetailsElement { Key = "Fragment", Data = new DetailsLink(uri.Fragment) });
}
}
catch
{
// ignore malformed inputs
}
return result;
}
public IEnumerable<ProviderAction> GetActions(ClipboardItem item)
{
if (!CanHandle(item))
{
yield break;
}
var normalized = UrlHelper.NormalizeUrl(item.Content!);
var open = new CommandContextItem(new OpenUrlCommand(normalized))
{
RequestedShortcut = KeyChords.OpenUrl,
};
yield return new ProviderAction(WellKnownActionIds.Open, open);
}
}

View File

@@ -1,16 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
/// <summary>
/// Well-known action id constants used to de-duplicate provider actions.
/// </summary>
internal static class WellKnownActionIds
{
public const string Open = "open";
public const string OpenLocation = "openLocation";
public const string CopyPath = "copyPath";
public const string OpenConsole = "openConsole";
}

View File

@@ -3,7 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.CmdPal.Core.Common.Helpers;
using System.IO;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
@@ -31,7 +31,7 @@ internal static class UrlHelper
}
// Check if it's a valid file path (local or network)
if (PathHelper.IsValidFilePath(url))
if (IsValidFilePath(url))
{
return true;
}
@@ -78,7 +78,7 @@ internal static class UrlHelper
url = url.Trim();
// If it's a valid file path, convert to file:// URI
if (!url.StartsWith("file://", StringComparison.OrdinalIgnoreCase) && PathHelper.IsValidFilePath(url))
if (IsValidFilePath(url) && !url.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
{
try
{
@@ -105,4 +105,40 @@ internal static class UrlHelper
return url;
}
/// <summary>
/// Checks if a string represents a valid file path (local or network)
/// </summary>
/// <param name="path">The string to check</param>
/// <returns>True if the string is a valid file path, false otherwise</returns>
private static bool IsValidFilePath(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return false;
}
try
{
// Check for UNC paths (network paths starting with \\)
if (path.StartsWith(@"\\", StringComparison.Ordinal))
{
// Basic UNC path validation: \\server\share or \\server\share\path
var parts = path.Substring(2).Split('\\', StringSplitOptions.RemoveEmptyEntries);
return parts.Length >= 2; // At minimum: server and share
}
// Check for drive letters (C:\ or C:)
if (path.Length >= 2 && char.IsLetter(path[0]) && path[1] == ':')
{
return true;
}
return false;
}
catch
{
return false;
}
}
}

View File

@@ -10,7 +10,6 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\Core\Microsoft.CmdPal.Core.Common\Microsoft.CmdPal.Core.Common.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -9,7 +9,6 @@ using System.Linq;
using Microsoft.CmdPal.Common.Commands;
using Microsoft.CmdPal.Ext.ClipboardHistory.Commands;
using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -17,20 +16,13 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory.Models;
internal sealed partial class ClipboardListItem : ListItem
{
private static readonly IClipboardMetadataProvider[] MetadataProviders =
[
new ImageMetadataProvider(),
new TextFileSystemMetadataProvider(),
new WebLinkMetadataProvider(),
new TextMetadataProvider(),
];
private readonly SettingsManager _settingsManager;
private readonly ClipboardItem _item;
private readonly CommandContextItem _deleteContextMenuItem;
private readonly CommandContextItem? _pasteCommand;
private readonly CommandContextItem? _copyCommand;
private readonly CommandContextItem? _openUrlCommand;
private readonly Lazy<Details> _lazyDetails;
public override IDetails? Details
@@ -81,11 +73,26 @@ internal sealed partial class ClipboardListItem : ListItem
_pasteCommand = new CommandContextItem(new PasteCommand(_item, ClipboardFormat.Text, _settingsManager));
_copyCommand = new CommandContextItem(new CopyCommand(_item, ClipboardFormat.Text));
// Check if the text content is a valid URL and add OpenUrl command
if (UrlHelper.IsValidUrl(_item.Content ?? string.Empty))
{
var normalizedUrl = UrlHelper.NormalizeUrl(_item.Content ?? string.Empty);
_openUrlCommand = new CommandContextItem(new OpenUrlCommand(normalizedUrl))
{
RequestedShortcut = KeyChords.OpenUrl,
};
}
else
{
_openUrlCommand = null;
}
}
else
{
_pasteCommand = null;
_copyCommand = null;
_openUrlCommand = null;
}
RefreshCommands();
@@ -156,74 +163,27 @@ internal sealed partial class ClipboardListItem : ListItem
commands.Add(firstCommand);
}
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var temp = new List<IContextItem>();
foreach (var provider in MetadataProviders)
if (_openUrlCommand != null)
{
if (!provider.CanHandle(_item))
{
continue;
}
foreach (var action in provider.GetActions(_item))
{
if (string.IsNullOrEmpty(action.Id) || !seen.Add(action.Id))
{
continue;
}
temp.Add(action.Action);
}
}
if (temp.Count > 0)
{
if (commands.Count > 0)
{
commands.Add(new Separator());
}
commands.AddRange(temp);
commands.Add(_openUrlCommand);
}
commands.Add(new Separator());
commands.Add(_deleteContextMenuItem);
return [.. commands];
return commands.ToArray();
}
private Details CreateDetails()
{
List<IDetailsElement> metadata = [];
foreach (var provider in MetadataProviders)
{
if (provider.CanHandle(_item))
IDetailsElement[] metadata =
[
new DetailsElement
{
var details = provider.GetDetails(_item);
if (details.Any())
{
metadata.Add(new DetailsElement
{
Key = provider.SectionTitle,
Data = new DetailsSeparator(),
});
metadata.AddRange(details);
}
Key = "Copied on",
Data = new DetailsLink(_item.Timestamp.DateTime.ToString(DateTimeFormatInfo.CurrentInfo)),
}
}
metadata.Add(new DetailsElement
{
Key = "General",
Data = new DetailsSeparator(),
});
metadata.Add(new DetailsElement
{
Key = "Copied",
Data = new DetailsLink(_item.Timestamp.DateTime.ToString(DateTimeFormatInfo.CurrentInfo)),
});
];
if (_item.IsImage)
{
@@ -233,7 +193,7 @@ internal sealed partial class ClipboardListItem : ListItem
{
Title = _item.GetDataType(),
HeroImage = heroImage,
Metadata = [.. metadata],
Metadata = metadata,
};
}
@@ -243,7 +203,7 @@ internal sealed partial class ClipboardListItem : ListItem
{
Title = _item.GetDataType(),
Body = $"```text\n{_item.Content}\n```",
Metadata = [.. metadata],
Metadata = metadata,
};
}

View File

@@ -324,7 +324,7 @@ internal sealed class Window
// Correct the process data if the window belongs to a uwp app hosted by 'ApplicationFrameHost.exe'
// (This only works if the window isn't minimized. For minimized windows the required child window isn't assigned.)
if (_handlesToProcessCache[hWindow].IsUwpAppFrameHost)
if (string.Equals(_handlesToProcessCache[hWindow].Name, "ApplicationFrameHost.exe", StringComparison.OrdinalIgnoreCase))
{
new Task(() =>
{

View File

@@ -23,7 +23,7 @@ internal sealed class WindowProcess
/// <summary>
/// An indicator if the window belongs to an 'Universal Windows Platform (UWP)' process
/// </summary>
private bool _isUwpAppFrameHost;
private readonly bool _isUwpAppFrameHost;
/// <summary>
/// Gets the id of the process
@@ -126,14 +126,6 @@ internal sealed class WindowProcess
get; private set;
}
/// <summary>
/// Gets the type of the process (UWP app, packaged Win32 app, unpackaged Win32 app, ...).
/// </summary>
internal ProcessPackagingInfo ProcessType
{
get; private set;
}
/// <summary>
/// Initializes a new instance of the <see cref="WindowProcess"/> class.
/// </summary>
@@ -142,10 +134,13 @@ internal sealed class WindowProcess
/// <param name="name">New process name.</param>
internal WindowProcess(uint pid, uint tid, string name)
{
ProcessType = ProcessPackagingInfo.Empty;
UpdateProcessInfo(pid, tid, name);
ProcessType = ProcessPackagingInspector.Inspect((int)pid);
_isUwpAppFrameHost = string.Equals(Name, "ApplicationFrameHost.exe", StringComparison.OrdinalIgnoreCase);
}
public ProcessPackagingInfo ProcessType { get; private set; }
/// <summary>
/// Updates the process information of the <see cref="WindowProcess"/> instance.
/// </summary>
@@ -161,10 +156,6 @@ internal sealed class WindowProcess
// Process can be elevated only if process id is not 0 (Dummy value on error)
IsFullAccessDenied = (pid != 0) ? TestProcessAccessUsingAllAccessFlag(pid) : false;
// Update process type
ProcessType = ProcessPackagingInspector.Inspect((int)pid);
_isUwpAppFrameHost = string.Equals(Name, "ApplicationFrameHost.exe", StringComparison.OrdinalIgnoreCase);
}
/// <summary>

View File

@@ -11,13 +11,4 @@ internal sealed record ProcessPackagingInfo(
bool IsAppContainer,
string? PackageFullName,
int? LastError
)
{
public static ProcessPackagingInfo Empty { get; } = new(
Pid: 0,
Kind: ProcessPackagingKind.Unknown,
HasPackageIdentity: false,
IsAppContainer: false,
PackageFullName: null,
LastError: null);
}
);

View File

@@ -11,7 +11,6 @@
<Import Project="..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.props')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.MSIX.1.7.20250829.1\build\Microsoft.Windows.SDK.BuildTools.MSIX.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.MSIX.1.7.20250829.1\build\Microsoft.Windows.SDK.BuildTools.MSIX.props')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<PropertyGroup Label="Globals">
<CppWinRTOptimized>true</CppWinRTOptimized>
@@ -215,8 +214,7 @@
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.MSIX.1.7.20250829.1\build\Microsoft.Windows.SDK.BuildTools.MSIX.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.MSIX.1.7.20250829.1\build\Microsoft.Windows.SDK.BuildTools.MSIX.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets')" />
@@ -237,8 +235,6 @@
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4188\build\Microsoft.Windows.SDK.BuildTools.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.MSIX.1.7.20250829.1\build\Microsoft.Windows.SDK.BuildTools.MSIX.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.MSIX.1.7.20250829.1\build\Microsoft.Windows.SDK.BuildTools.MSIX.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.MSIX.1.7.20250829.1\build\Microsoft.Windows.SDK.BuildTools.MSIX.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.MSIX.1.7.20250829.1\build\Microsoft.Windows.SDK.BuildTools.MSIX.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.targets'))" />

View File

@@ -65,17 +65,6 @@ public partial class ImageSize : INotifyPropertyChanged, IHasId
get => !(Unit == ResizeUnit.Percent && Fit != ResizeFit.Stretch);
}
/// <summary>
/// Gets the localized header text for the Width field. When in Percent mode (non-stretch),
/// returns "Percent" since the value represents a scale factor for both dimensions.
/// Otherwise returns "Width".
/// </summary>
[JsonIgnore]
public string WidthHeader
{
get => !IsHeightUsed ? ResourceLoader.GetString("ImageResizer_Sizes_Units_Percent") : ResourceLoader.GetString("ImageResizer_Width");
}
[JsonPropertyName("name")]
public string Name
{
@@ -92,7 +81,6 @@ public partial class ImageSize : INotifyPropertyChanged, IHasId
if (SetProperty(ref _fit, value))
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsHeightUsed)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(WidthHeader)));
}
}
}
@@ -117,18 +105,9 @@ public partial class ImageSize : INotifyPropertyChanged, IHasId
get => _unit;
set
{
var previousUnit = _unit;
if (SetProperty(ref _unit, value))
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsHeightUsed)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(WidthHeader)));
// When switching to Percent unit, reset width and height to 100%
if (value == ResizeUnit.Percent && previousUnit != ResizeUnit.Percent)
{
Width = 100;
Height = 100;
}
}
}
}

View File

@@ -134,8 +134,8 @@
<StackPanel Orientation="Horizontal" Spacing="8">
<controls:ImageResizerDimensionsNumberBox
x:Uid="ImageResizer_Width"
Width="116"
Header="{x:Bind WidthHeader, Mode=OneWay}"
Minimum="0"
SpinButtonPlacementMode="Compact"
Value="{x:Bind Width, Mode=TwoWay, Converter={StaticResource ImageResizerNumberBoxValueConverter}}" />

View File

@@ -52,8 +52,10 @@ function RunMSBuild {
$base = @(
$Solution
"/m"
"/p:Platform=$Platform"
"/p:Configuration=$Configuration"
"/p:BuildInParallel=true"
"/verbosity:normal"
'/clp:Summary;PerformanceSummary;ErrorsOnly;WarningsOnly'
"/fileLoggerParameters:LogFile=$allLog;Verbosity=detailed"
@@ -92,9 +94,7 @@ function RestoreThenBuild {
RunMSBuild $Solution $restoreArgs $Platform $Configuration
if (-not $RestoreOnly) {
$buildArgs = '/m'
if ($ExtraArgs) { $buildArgs = "$buildArgs $ExtraArgs" }
RunMSBuild $Solution $buildArgs $Platform $Configuration
RunMSBuild $Solution $ExtraArgs $Platform $Configuration
}
}
@@ -133,9 +133,7 @@ function BuildProjectsInDirectory {
if ($f.Extension -eq '.sln') {
RestoreThenBuild $f.FullName $ExtraArgs $Platform $Configuration $RestoreOnly
} else {
$buildArgs = '/m'
if ($ExtraArgs) { $buildArgs = "$buildArgs $ExtraArgs" }
RunMSBuild $f.FullName $buildArgs $Platform $Configuration
RunMSBuild $f.FullName $ExtraArgs $Platform $Configuration
}
}

View File

@@ -63,6 +63,7 @@ if ($ForceRebuildExecutable -or -not (Test-Path $exePath)) {
'/m',
"/p:Configuration=$BuildConfiguration",
"/p:Platform=x64",
"/p:BuildInParallel=true",
'/restore'
)