Merge branch 'main' of https://github.com/microsoft/PowerToys into leilzh/adclean

This commit is contained in:
Leilei Zhang
2025-11-11 10:14:02 +08:00
61 changed files with 2053 additions and 594 deletions

View File

@@ -105,6 +105,7 @@
^src/common/notifications/BackgroundActivatorDLL/cpp\.hint$ ^src/common/notifications/BackgroundActivatorDLL/cpp\.hint$
^src/common/sysinternals/Eula/ ^src/common/sysinternals/Eula/
^src/modules/cmdpal/doc/initial-sdk-spec/list-elements-mock-002\.pdn$ ^src/modules/cmdpal/doc/initial-sdk-spec/list-elements-mock-002\.pdn$
^src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleMarkdownImagesPage\.cs$
^src/modules/colorPicker/ColorPickerUI/Shaders/GridShader\.cso$ ^src/modules/colorPicker/ColorPickerUI/Shaders/GridShader\.cso$
^src/modules/launcher/Plugins/Microsoft\.PowerToys\.Run\.Plugin\.TimeDate/Properties/ ^src/modules/launcher/Plugins/Microsoft\.PowerToys\.Run\.Plugin\.TimeDate/Properties/
^src/modules/MouseUtils/MouseJumpUI/MainForm\.resx$ ^src/modules/MouseUtils/MouseJumpUI/MainForm\.resx$

View File

@@ -65,6 +65,7 @@ APIIs
Apm Apm
APPBARDATA APPBARDATA
APPEXECLINK APPEXECLINK
appext
APPLICATIONFRAMEHOST APPLICATIONFRAMEHOST
appmanifest appmanifest
APPMODEL APPMODEL
@@ -100,7 +101,6 @@ ATX
ATRIOX ATRIOX
aumid aumid
authenticode authenticode
Authenticode
AUTOBUDDY AUTOBUDDY
AUTOCHECKBOX AUTOCHECKBOX
AUTOHIDE AUTOHIDE
@@ -187,6 +187,7 @@ CAPTUREBLT
CAPTURECHANGED CAPTURECHANGED
CARETBLINKING CARETBLINKING
CAtl CAtl
CBN
cch cch
CCHDEVICENAME CCHDEVICENAME
CCHFORMNAME CCHFORMNAME
@@ -315,7 +316,6 @@ CURSORINFO
cursorpos cursorpos
CURSORSHOWING CURSORSHOWING
CURSORWRAP CURSORWRAP
CursorWrap
customaction customaction
CUSTOMACTIONTEST CUSTOMACTIONTEST
CUSTOMFORMATPLACEHOLDER CUSTOMFORMATPLACEHOLDER
@@ -415,6 +415,9 @@ DNLEN
DONOTROUND DONOTROUND
DONTVALIDATEPATH DONTVALIDATEPATH
dotnet dotnet
downsampled
downsampling
Downsampled
downscale downscale
DPICHANGED DPICHANGED
DPIs DPIs
@@ -431,7 +434,6 @@ DSTINVERT
DString DString
DSVG DSVG
dto dto
DTo
DUMMYUNIONNAME DUMMYUNIONNAME
dutil dutil
DVASPECT DVASPECT
@@ -465,7 +467,6 @@ EDITKEYBOARD
EDITSHORTCUTS EDITSHORTCUTS
EDITTEXT EDITTEXT
EFile EFile
ekus
eku eku
emojis emojis
ENABLEDELAYEDEXPANSION ENABLEDELAYEDEXPANSION
@@ -601,6 +602,7 @@ getfilesiginforedist
geolocator geolocator
GETHOTKEY GETHOTKEY
GETICON GETICON
GETLBTEXT
GETMINMAXINFO GETMINMAXINFO
GETNONCLIENTMETRICS GETNONCLIENTMETRICS
GETPROPERTYSTOREFLAGS GETPROPERTYSTOREFLAGS
@@ -608,6 +610,7 @@ GETSCREENSAVERRUNNING
GETSECKEY GETSECKEY
GETSTICKYKEYS GETSTICKYKEYS
GETTEXTLENGTH GETTEXTLENGTH
GIFs
gitmodules gitmodules
GHND GHND
GMEM GMEM
@@ -618,6 +621,7 @@ GPOCA
gpp gpp
gpu gpu
gradians gradians
grctlext
Gridcustomlayout Gridcustomlayout
GSM GSM
gtm gtm
@@ -1150,7 +1154,6 @@ NONCLIENTMETRICSW
NONELEVATED NONELEVATED
nonspace nonspace
nonstd nonstd
nullrefs
NOOWNERZORDER NOOWNERZORDER
NOPARENTNOTIFY NOPARENTNOTIFY
NOPREFIX NOPREFIX
@@ -1190,8 +1193,8 @@ ntfs
NTSTATUS NTSTATUS
NTSYSAPI NTSYSAPI
NULLCURSOR NULLCURSOR
nullref
nullonfailure nullonfailure
nullref
numberbox numberbox
nwc nwc
ocr ocr

View File

@@ -253,7 +253,7 @@ _SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
# hit-count: 1 file-count: 1 # hit-count: 1 file-count: 1
# Amazon # Amazon
\bamazon\.com/[-\w]+/(?:dp/[0-9A-Z]+|)[^"'\s]+ \bamazon\.com/[-\w]+/(?:dp/[0-9A-Z]+|)
# hit-count: 3 file-count: 3 # hit-count: 3 file-count: 3
# imgur # imgur

View File

@@ -52,8 +52,6 @@ extends:
name: SHINE-INT-S name: SHINE-INT-S
${{ if eq(parameters.useVSPreview, true) }}: ${{ if eq(parameters.useVSPreview, true) }}:
demands: ImageOverride -equals SHINE-VS17-Preview demands: ImageOverride -equals SHINE-VS17-Preview
${{ else }}:
image: SHINE-VS17-Latest
os: windows os: windows
sdl: sdl:
tsa: tsa:
@@ -75,7 +73,6 @@ extends:
name: SHINE-INT-L name: SHINE-INT-L
demands: demands:
# Our INT agents have a large disk mounted at P:\ # Our INT agents have a large disk mounted at P:\
- WorkFolder -equals P:\_work
- ${{ if eq(parameters.useVSPreview, true) }}: - ${{ if eq(parameters.useVSPreview, true) }}:
- ImageOverride -equals SHINE-VS17-Preview - ImageOverride -equals SHINE-VS17-Preview
os: windows os: windows
@@ -126,7 +123,6 @@ extends:
parameters: parameters:
pool: pool:
name: SHINE-INT-L name: SHINE-INT-L
image: SHINE-VS17-Latest
os: windows os: windows
official: true official: true
codeSign: true codeSign: true

View File

@@ -111,6 +111,7 @@ jobs:
${{ else }}: ${{ else }}:
OutputBuildPlatform: ${{ platform }} OutputBuildPlatform: ${{ platform }}
variables: variables:
NUGET_PACKAGES: 'C:\NuGetPackages' # Some of our build steps cache these here... and it was apparently part of the global environment
MakeAppxPath: 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x86\MakeAppx.exe' MakeAppxPath: 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x86\MakeAppx.exe'
# Azure DevOps abhors a vacuum # Azure DevOps abhors a vacuum
# If these are blank, expansion will fail later on... which will result in direct substitution of the variable *names* # If these are blank, expansion will fail later on... which will result in direct substitution of the variable *names*
@@ -139,6 +140,10 @@ jobs:
- output: pipelineArtifact - output: pipelineArtifact
artifactName: $(JobOutputArtifactName) artifactName: $(JobOutputArtifactName)
targetPath: $(Build.ArtifactStagingDirectory) targetPath: $(Build.ArtifactStagingDirectory)
- output: pipelineArtifact
artifactName: $(JobOutputArtifactName)-failure-$(System.JobAttempt)
targetPath: $(LogOutputDirectory)
condition: or(failed(), canceled())
steps: steps:
- checkout: self - checkout: self
clean: true clean: true
@@ -395,7 +400,7 @@ jobs:
### HACK: On ARM64 builds, building an app with Windows App SDK copies the x64 WebView2 dll instead of the ARM64 one. This task makes sure the right dll is used. ### HACK: On ARM64 builds, building an app with Windows App SDK copies the x64 WebView2 dll instead of the ARM64 one. This task makes sure the right dll is used.
- task: CopyFiles@2 - task: CopyFiles@2
displayName: HACK Copy core WebView2 ARM64 dll to output directory displayName: HACK Copy core WebView2 ARM64 dll to output directory
condition: eq(variables['BuildPlatform'],'arm64') condition: and(succeeded(), eq(variables['BuildPlatform'], 'arm64'))
inputs: inputs:
contents: packages/Microsoft.Web.WebView2.1.0.2903.40/runtimes/win-ARM64/native_uap/Microsoft.Web.WebView2.Core.dll contents: packages/Microsoft.Web.WebView2.1.0.2903.40/runtimes/win-ARM64/native_uap/Microsoft.Web.WebView2.Core.dll
targetFolder: $(Build.SourcesDirectory)/ARM64/Release/WinUI3Apps/ targetFolder: $(Build.SourcesDirectory)/ARM64/Release/WinUI3Apps/
@@ -434,11 +439,11 @@ jobs:
inputs: inputs:
testResultsFormat: VSTest testResultsFormat: VSTest
testResultsFiles: '**/*.trx' testResultsFiles: '**/*.trx'
condition: ne(variables['BuildPlatform'],'arm64') condition: and(succeeded(), ne(variables['BuildPlatform'], 'arm64'))
# Native dlls # Native dlls
- task: VSTest@2 - task: VSTest@2
condition: ne(variables['BuildPlatform'],'arm64') # No arm64 agents to run the tests. condition: and(succeeded(), ne(variables['BuildPlatform'], 'arm64')) # No arm64 agents to run the tests.
displayName: 'Native Tests' displayName: 'Native Tests'
inputs: inputs:
platform: '$(BuildPlatform)' platform: '$(BuildPlatform)'

View File

@@ -10,11 +10,11 @@
<h3 align="center"> <h3 align="center">
<a href="#-installation">Installation</a> <a href="#-installation">Installation</a>
<span> . </span> <span> · </span>
<a href="https://aka.ms/powertoys-docs">Documentation</a> <a href="https://aka.ms/powertoys-docs">Documentation</a>
<span> . </span> <span> · </span>
<a href="https://aka.ms/powertoys-releaseblog">Blog</a> <a href="https://aka.ms/powertoys-releaseblog">Blog</a>
<span> . </span> <span> · </span>
<a href="#-whats-new">Release notes</a> <a href="#-whats-new">Release notes</a>
</h3> </h3>
<br/><br/> <br/><br/>

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1,175 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text.Json;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
namespace Microsoft.PowerToys.UITest
{
/// <summary>
/// Helper class for configuring PowerToys settings for UI tests.
/// </summary>
public class SettingsConfigHelper
{
private static readonly JsonSerializerOptions IndentedJsonOptions = new() { WriteIndented = true };
private static readonly SettingsUtils SettingsUtils = new SettingsUtils();
/// <summary>
/// Configures global PowerToys settings to enable only specified modules and disable all others.
/// </summary>
/// <param name="modulesToEnable">Array of module names to enable (e.g., "Peek", "FancyZones"). All other modules will be disabled.</param>
/// <exception cref="ArgumentNullException">Thrown when modulesToEnable is null.</exception>
/// <exception cref="InvalidOperationException">Thrown when settings file operations fail.</exception>
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "This is test code and will not be trimmed")]
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "This is test code and will not be AOT compiled")]
public static void ConfigureGlobalModuleSettings(params string[] modulesToEnable)
{
ArgumentNullException.ThrowIfNull(modulesToEnable);
try
{
GeneralSettings settings;
try
{
settings = SettingsUtils.GetSettingsOrDefault<GeneralSettings>();
}
catch (Exception ex)
{
Debug.WriteLine($"Failed to load settings, creating defaults: {ex.Message}");
settings = new GeneralSettings();
}
string settingsJson = settings.ToJsonString();
using (JsonDocument doc = JsonDocument.Parse(settingsJson))
{
var options = new JsonSerializerOptions { WriteIndented = true };
var root = doc.RootElement.Clone();
if (root.TryGetProperty("enabled", out var enabledElement))
{
var enabledModules = new Dictionary<string, bool>();
foreach (var property in enabledElement.EnumerateObject())
{
string moduleName = property.Name;
bool shouldEnable = Array.Exists(modulesToEnable, m => string.Equals(m, moduleName, StringComparison.Ordinal));
enabledModules[moduleName] = shouldEnable;
}
var settingsDict = JsonSerializer.Deserialize<Dictionary<string, object>>(settingsJson);
if (settingsDict != null)
{
settingsDict["enabled"] = enabledModules;
settingsJson = JsonSerializer.Serialize(settingsDict, IndentedJsonOptions);
}
}
}
SettingsUtils.SaveSettings(settingsJson);
string enabledList = modulesToEnable.Length > 0 ? string.Join(", ", modulesToEnable) : "none";
Debug.WriteLine($"Successfully updated global settings");
Debug.WriteLine($"Enabled modules: {enabledList}");
}
catch (Exception ex)
{
Debug.WriteLine($"ERROR in ConfigureGlobalModuleSettings: {ex.Message}");
throw new InvalidOperationException($"Failed to configure global module settings: {ex.Message}", ex);
}
}
/// <summary>
/// Updates a module's settings file. If the file doesn't exist, creates it with default content.
/// If the file exists, reads it and applies the provided update function to modify the settings.
/// </summary>
/// <param name="moduleName">The name of the module (e.g., "Peek", "FancyZones").</param>
/// <param name="defaultSettingsContent">The default JSON content to use if the settings file doesn't exist.</param>
/// <param name="updateSettingsAction">
/// A callback function that modifies the settings dictionary. The function receives the deserialized settings
/// and should modify it in-place. The function should accept a Dictionary&lt;string, object&gt; and not return a value.
/// Example: (settings) => { ((Dictionary&lt;string, object&gt;)settings["properties"])["SomeSetting"] = newValue; }
/// </param>
/// <exception cref="ArgumentNullException">Thrown when moduleName or updateSettingsAction is null.</exception>
/// <exception cref="InvalidOperationException">Thrown when settings file operations fail.</exception>
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "This is test code and will not be trimmed")]
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "This is test code and will not be AOT compiled")]
public static void UpdateModuleSettings(
string moduleName,
string defaultSettingsContent,
Action<Dictionary<string, object>> updateSettingsAction)
{
ArgumentNullException.ThrowIfNull(moduleName);
ArgumentNullException.ThrowIfNull(updateSettingsAction);
try
{
// Build the path to the module settings file
string powerToysSettingsDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Microsoft",
"PowerToys");
string moduleDirectory = Path.Combine(powerToysSettingsDirectory, moduleName);
string settingsPath = Path.Combine(moduleDirectory, "settings.json");
// Ensure directory exists
Directory.CreateDirectory(moduleDirectory);
// Read existing settings or use default
string existingJson = string.Empty;
if (File.Exists(settingsPath))
{
existingJson = File.ReadAllText(settingsPath);
}
Dictionary<string, object>? settings;
// If file doesn't exist or is empty, create from defaults
if (string.IsNullOrWhiteSpace(existingJson))
{
if (string.IsNullOrWhiteSpace(defaultSettingsContent))
{
throw new ArgumentException("Default settings content must be provided when file doesn't exist.", nameof(defaultSettingsContent));
}
settings = JsonSerializer.Deserialize<Dictionary<string, object>>(defaultSettingsContent)
?? throw new InvalidOperationException($"Failed to deserialize default settings for {moduleName}");
Debug.WriteLine($"Created default settings for {moduleName} at {settingsPath}");
}
else
{
// Parse existing settings
settings = JsonSerializer.Deserialize<Dictionary<string, object>>(existingJson)
?? throw new InvalidOperationException($"Failed to deserialize existing settings for {moduleName}");
Debug.WriteLine($"Loaded existing settings for {moduleName} from {settingsPath}");
}
// Apply the update action to modify settings
updateSettingsAction(settings);
// Serialize and save the updated settings using SettingsUtils
string updatedJson = JsonSerializer.Serialize(settings, IndentedJsonOptions);
SettingsUtils.SaveSettings(updatedJson, moduleName);
Debug.WriteLine($"Successfully updated settings for {moduleName}");
}
catch (Exception ex)
{
Debug.WriteLine($"ERROR in UpdateModuleSettings for {moduleName}: {ex.Message}");
throw new InvalidOperationException($"Failed to update settings for {moduleName}: {ex.Message}", ex);
}
}
}
}

View File

@@ -8,7 +8,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<PublishAot>true</PublishAot> <PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization> <InvariantGlobalization>true</InvariantGlobalization>
<TargetFramework>net9.0-windows10.0.22621.0</TargetFramework> <TargetFramework>net9.0-windows10.0.26100.0</TargetFramework>
<UseWindowsForms>true</UseWindowsForms> <UseWindowsForms>true</UseWindowsForms>
<PublishTrimmed>false</PublishTrimmed> <PublishTrimmed>false</PublishTrimmed>
</PropertyGroup> </PropertyGroup>
@@ -21,4 +21,8 @@
<PackageReference Include="CoenM.ImageSharp.ImageHash" /> <PackageReference Include="CoenM.ImageSharp.ImageHash" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
</ItemGroup>
</Project> </Project>

View File

@@ -542,7 +542,10 @@
Source="{x:Bind ViewModel.ActiveAIProvider?.ServiceType, Mode=OneWay, Converter={StaticResource ServiceTypeToIconConverter}}" /> Source="{x:Bind ViewModel.ActiveAIProvider?.ServiceType, Mode=OneWay, Converter={StaticResource ServiceTypeToIconConverter}}" />
</DropDownButton.Content> </DropDownButton.Content>
<DropDownButton.Flyout> <DropDownButton.Flyout>
<Flyout Placement="Bottom" ShouldConstrainToRootBounds="False"> <Flyout
Opened="AIProviderFlyout_Opened"
Placement="Bottom"
ShouldConstrainToRootBounds="False">
<Grid <Grid
Width="386" Width="386"
Margin="-4" Margin="-4"

View File

@@ -22,6 +22,8 @@ namespace AdvancedPaste.Controls
{ {
public OptionsViewModel ViewModel { get; private set; } public OptionsViewModel ViewModel { get; private set; }
private bool _syncingProviderSelection;
public static readonly DependencyProperty PlaceholderTextProperty = DependencyProperty.Register( public static readonly DependencyProperty PlaceholderTextProperty = DependencyProperty.Register(
nameof(PlaceholderText), nameof(PlaceholderText),
typeof(string), typeof(string),
@@ -74,6 +76,11 @@ namespace AdvancedPaste.Controls
var state = ViewModel.IsBusy ? "LoadingState" : ViewModel.PasteActionError.HasText ? "ErrorState" : "DefaultState"; var state = ViewModel.IsBusy ? "LoadingState" : ViewModel.PasteActionError.HasText ? "ErrorState" : "DefaultState";
VisualStateManager.GoToState(this, state, true); VisualStateManager.GoToState(this, state, true);
} }
if (e.PropertyName is nameof(ViewModel.ActiveAIProvider) or nameof(ViewModel.AIProviders))
{
SyncProviderSelection();
}
} }
private void ViewModel_PreviewRequested(object sender, EventArgs e) private void ViewModel_PreviewRequested(object sender, EventArgs e)
@@ -87,6 +94,7 @@ namespace AdvancedPaste.Controls
private void Grid_Loaded(object sender, RoutedEventArgs e) private void Grid_Loaded(object sender, RoutedEventArgs e)
{ {
InputTxtBox.Focus(FocusState.Programmatic); InputTxtBox.Focus(FocusState.Programmatic);
SyncProviderSelection();
} }
[RelayCommand] [RelayCommand]
@@ -126,18 +134,56 @@ namespace AdvancedPaste.Controls
Loader.IsLoading = loading; Loader.IsLoading = loading;
} }
private void SyncProviderSelection()
{
if (AIProviderListView is null)
{
return;
}
try
{
_syncingProviderSelection = true;
AIProviderListView.SelectedItem = ViewModel.ActiveAIProvider;
}
finally
{
_syncingProviderSelection = false;
}
}
private void AIProviderFlyout_Opened(object sender, object e)
{
SyncProviderSelection();
}
private async void AIProviderListView_SelectionChanged(object sender, SelectionChangedEventArgs e) private async void AIProviderListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{ {
if (AIProviderListView.SelectedItem is PasteAIProviderDefinition provider) if (_syncingProviderSelection)
{ {
if (ViewModel.SetActiveProviderCommand.CanExecute(provider)) return;
{
await ViewModel.SetActiveProviderCommand.ExecuteAsync(provider);
}
var flyout = FlyoutBase.GetAttachedFlyout(AIProviderButton);
flyout?.Hide();
} }
var flyout = FlyoutBase.GetAttachedFlyout(AIProviderButton);
if (AIProviderListView.SelectedItem is not PasteAIProviderDefinition provider)
{
return;
}
if (string.Equals(ViewModel.ActiveAIProvider?.Id, provider.Id, StringComparison.OrdinalIgnoreCase))
{
flyout?.Hide();
return;
}
if (ViewModel.SetActiveProviderCommand.CanExecute(provider))
{
await ViewModel.SetActiveProviderCommand.ExecuteAsync(provider);
SyncProviderSelection();
}
flyout?.Hide();
} }
} }
} }

View File

@@ -280,13 +280,15 @@
x:Uid="TermsLink" x:Uid="TermsLink"
Padding="0" Padding="0"
FontSize="12" FontSize="12"
NavigateUri="https://openai.com/policies/terms-of-use" /> NavigateUri="{x:Bind ViewModel.TermsLinkUri, Mode=OneWay}"
Visibility="{x:Bind ViewModel.HasTermsLink, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
<HyperlinkButton <HyperlinkButton
x:Name="PrivacyHyperLink" x:Name="PrivacyHyperLink"
x:Uid="PrivacyLink" x:Uid="PrivacyLink"
Padding="0" Padding="0"
FontSize="12" FontSize="12"
NavigateUri="https://openai.com/policies/privacy-policy" /> NavigateUri="{x:Bind ViewModel.PrivacyLinkUri, Mode=OneWay}"
Visibility="{x:Bind ViewModel.HasPrivacyLink, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</Flyout> </Flyout>

View File

@@ -71,6 +71,11 @@ namespace AdvancedPaste.ViewModels
[NotifyPropertyChangedFor(nameof(IsCustomAIAvailable))] [NotifyPropertyChangedFor(nameof(IsCustomAIAvailable))]
[NotifyPropertyChangedFor(nameof(AllowedAIProviders))] [NotifyPropertyChangedFor(nameof(AllowedAIProviders))]
[NotifyPropertyChangedFor(nameof(ActiveAIProvider))] [NotifyPropertyChangedFor(nameof(ActiveAIProvider))]
[NotifyPropertyChangedFor(nameof(ActiveAIProviderTooltip))]
[NotifyPropertyChangedFor(nameof(TermsLinkUri))]
[NotifyPropertyChangedFor(nameof(PrivacyLinkUri))]
[NotifyPropertyChangedFor(nameof(HasTermsLink))]
[NotifyPropertyChangedFor(nameof(HasPrivacyLink))]
private bool _isAllowedByGPO; private bool _isAllowedByGPO;
[ObservableProperty] [ObservableProperty]
@@ -187,6 +192,35 @@ namespace AdvancedPaste.ViewModels
} }
} }
private AIServiceTypeMetadata GetActiveProviderMetadata()
{
var provider = ActiveAIProvider ?? AllowedAIProviders.FirstOrDefault();
var serviceType = provider?.ServiceTypeKind ?? AIServiceType.OpenAI;
return AIServiceTypeRegistry.GetMetadata(serviceType);
}
public Uri TermsLinkUri
{
get
{
var metadata = GetActiveProviderMetadata();
return metadata.HasTermsLink ? metadata.TermsUri : null;
}
}
public Uri PrivacyLinkUri
{
get
{
var metadata = GetActiveProviderMetadata();
return metadata.HasPrivacyLink ? metadata.PrivacyUri : null;
}
}
public bool HasTermsLink => GetActiveProviderMetadata().HasTermsLink;
public bool HasPrivacyLink => GetActiveProviderMetadata().HasPrivacyLink;
public bool ClipboardHasData => AvailableClipboardFormats != ClipboardFormat.None; public bool ClipboardHasData => AvailableClipboardFormats != ClipboardFormat.None;
public bool ClipboardHasDataForCustomAI => PasteFormat.SupportsClipboardFormats(CustomAIFormat, AvailableClipboardFormats); public bool ClipboardHasDataForCustomAI => PasteFormat.SupportsClipboardFormats(CustomAIFormat, AvailableClipboardFormats);
@@ -276,8 +310,8 @@ namespace AdvancedPaste.ViewModels
OnPropertyChanged(nameof(IsAdvancedAIEnabled)); OnPropertyChanged(nameof(IsAdvancedAIEnabled));
OnPropertyChanged(nameof(AIProviders)); OnPropertyChanged(nameof(AIProviders));
OnPropertyChanged(nameof(AllowedAIProviders)); OnPropertyChanged(nameof(AllowedAIProviders));
OnPropertyChanged(nameof(ActiveAIProvider));
OnPropertyChanged(nameof(ActiveAIProviderTooltip)); NotifyActiveProviderChanged();
EnqueueRefreshPasteFormats(); EnqueueRefreshPasteFormats();
} }
@@ -316,8 +350,17 @@ namespace AdvancedPaste.ViewModels
} }
} }
NotifyActiveProviderChanged();
}
private void NotifyActiveProviderChanged()
{
OnPropertyChanged(nameof(ActiveAIProvider)); OnPropertyChanged(nameof(ActiveAIProvider));
OnPropertyChanged(nameof(ActiveAIProviderTooltip)); OnPropertyChanged(nameof(ActiveAIProviderTooltip));
OnPropertyChanged(nameof(TermsLinkUri));
OnPropertyChanged(nameof(PrivacyLinkUri));
OnPropertyChanged(nameof(HasTermsLink));
OnPropertyChanged(nameof(HasPrivacyLink));
} }
private void RefreshPasteFormats() private void RefreshPasteFormats()
@@ -836,6 +879,7 @@ namespace AdvancedPaste.ViewModels
UpdateAIProviderActiveFlags(); UpdateAIProviderActiveFlags();
OnPropertyChanged(nameof(AIProviders)); OnPropertyChanged(nameof(AIProviders));
NotifyActiveProviderChanged();
EnqueueRefreshPasteFormats(); EnqueueRefreshPasteFormats();
} }

View File

@@ -46,7 +46,7 @@
<PreprocessorDefinitions>_DEBUG;NEWPLUSSHELLEXTENSIONWIN10_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions> <PreprocessorDefinitions>_DEBUG;NEWPLUSSHELLEXTENSIONWIN10_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode> <ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader> <PrecompiledHeader>Use</PrecompiledHeader>
<AdditionalIncludeDirectories>..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories);..\NewShellExtensionContextMenu</AdditionalIncludeDirectories> <AdditionalIncludeDirectories>..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories);..\NewShellExtensionContextMenu;$(MSBuildThisFileDirectory)Generated Files</AdditionalIncludeDirectories>
<CompileAsWinRT>false</CompileAsWinRT> <CompileAsWinRT>false</CompileAsWinRT>
<LanguageStandard>stdcpplatest</LanguageStandard> <LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile> </ClCompile>
@@ -67,7 +67,7 @@
<PreprocessorDefinitions>NDEBUG;NEWPLUSSHELLEXTENSIONWIN10_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions> <PreprocessorDefinitions>NDEBUG;NEWPLUSSHELLEXTENSIONWIN10_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode> <ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader> <PrecompiledHeader>Use</PrecompiledHeader>
<AdditionalIncludeDirectories>..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories);..\NewShellExtensionContextMenu</AdditionalIncludeDirectories> <AdditionalIncludeDirectories>..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories);..\NewShellExtensionContextMenu;$(MSBuildThisFileDirectory)Generated Files</AdditionalIncludeDirectories>
<CompileAsWinRT>false</CompileAsWinRT> <CompileAsWinRT>false</CompileAsWinRT>
<LanguageStandard>stdcpplatest</LanguageStandard> <LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile> </ClCompile>

View File

@@ -0,0 +1,548 @@
//==============================================================================
//
// Zoomit
// Sysinternals - www.sysinternals.com
//
// GIF recording support using Windows Imaging Component (WIC)
//
//==============================================================================
#include "pch.h"
#include "GifRecordingSession.h"
#include "CaptureFrameWait.h"
#include <shcore.h>
extern DWORD g_RecordScaling;
namespace winrt
{
using namespace Windows::Foundation;
using namespace Windows::Graphics;
using namespace Windows::Graphics::Capture;
using namespace Windows::Graphics::DirectX;
using namespace Windows::Graphics::DirectX::Direct3D11;
using namespace Windows::Storage;
using namespace Windows::UI::Composition;
}
namespace util
{
using namespace robmikh::common::uwp;
}
const float CLEAR_COLOR[] = { 0.0f, 0.0f, 0.0f, 1.0f };
int32_t EnsureEvenGif(int32_t value)
{
if (value % 2 == 0)
{
return value;
}
else
{
return value + 1;
}
}
//----------------------------------------------------------------------------
//
// GifRecordingSession::GifRecordingSession
//
//----------------------------------------------------------------------------
GifRecordingSession::GifRecordingSession(
winrt::IDirect3DDevice const& device,
winrt::GraphicsCaptureItem const& item,
RECT const cropRect,
uint32_t frameRate,
winrt::Streams::IRandomAccessStream const& stream)
{
m_device = device;
m_d3dDevice = GetDXGIInterfaceFromObject<ID3D11Device>(m_device);
m_d3dDevice->GetImmediateContext(m_d3dContext.put());
m_item = item;
m_frameRate = frameRate;
m_stream = stream;
auto itemSize = item.Size();
auto inputWidth = EnsureEvenGif(itemSize.Width);
auto inputHeight = EnsureEvenGif(itemSize.Height);
m_frameWait = std::make_shared<CaptureFrameWait>(m_device, m_item, winrt::SizeInt32{ inputWidth, inputHeight });
auto weakPointer{ std::weak_ptr{ m_frameWait } };
m_itemClosed = item.Closed(winrt::auto_revoke, [weakPointer](auto&, auto&)
{
auto sharedPointer{ weakPointer.lock() };
if (sharedPointer)
{
sharedPointer->StopCapture();
}
});
// Get crop dimension
if ((cropRect.right - cropRect.left) != 0)
{
m_rcCrop = cropRect;
m_frameWait->ShowCaptureBorder(false);
}
else
{
m_rcCrop.left = 0;
m_rcCrop.top = 0;
m_rcCrop.right = inputWidth;
m_rcCrop.bottom = inputHeight;
}
// Apply scaling
constexpr int c_minimumSize = 34;
auto scaledWidth = MulDiv(m_rcCrop.right - m_rcCrop.left, g_RecordScaling, 100);
auto scaledHeight = MulDiv(m_rcCrop.bottom - m_rcCrop.top, g_RecordScaling, 100);
m_width = scaledWidth;
m_height = scaledHeight;
if (m_width < c_minimumSize)
{
m_width = c_minimumSize;
m_height = MulDiv(m_height, m_width, scaledWidth);
}
if (m_height < c_minimumSize)
{
m_height = c_minimumSize;
m_width = MulDiv(m_width, m_height, scaledHeight);
}
if (m_width > inputWidth)
{
m_width = inputWidth;
m_height = c_minimumSize, MulDiv(m_height, scaledWidth, m_width);
}
if (m_height > inputHeight)
{
m_height = inputHeight;
m_width = c_minimumSize, MulDiv(m_width, scaledHeight, m_height);
}
m_width = EnsureEvenGif(m_width);
m_height = EnsureEvenGif(m_height);
m_frameDelay = (frameRate > 0) ? (100 / frameRate) : 15;
// Initialize WIC
winrt::check_hresult(CoCreateInstance(
CLSID_WICImagingFactory,
nullptr,
CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(m_wicFactory.put())));
// Create WIC stream from IRandomAccessStream
winrt::check_hresult(m_wicFactory->CreateStream(m_wicStream.put()));
// Get the IStream from the IRandomAccessStream
winrt::com_ptr<IStream> streamInterop;
winrt::check_hresult(CreateStreamOverRandomAccessStream(
winrt::get_unknown(stream),
IID_PPV_ARGS(streamInterop.put())));
winrt::check_hresult(m_wicStream->InitializeFromIStream(streamInterop.get()));
// Create GIF encoder
winrt::check_hresult(m_wicFactory->CreateEncoder(
GUID_ContainerFormatGif,
nullptr,
m_gifEncoder.put()));
winrt::check_hresult(m_gifEncoder->Initialize(m_wicStream.get(), WICBitmapEncoderNoCache));
// Set global GIF metadata for looping (NETSCAPE2.0 application extension)
try
{
winrt::com_ptr<IWICMetadataQueryWriter> encoderMetadataWriter;
if (SUCCEEDED(m_gifEncoder->GetMetadataQueryWriter(encoderMetadataWriter.put())) && encoderMetadataWriter)
{
OutputDebugStringW(L"Setting NETSCAPE2.0 looping extension on encoder...\n");
// Set application extension
PROPVARIANT propValue;
PropVariantInit(&propValue);
propValue.vt = VT_UI1 | VT_VECTOR;
propValue.caub.cElems = 11;
propValue.caub.pElems = static_cast<UCHAR*>(CoTaskMemAlloc(11));
if (propValue.caub.pElems != nullptr)
{
memcpy(propValue.caub.pElems, "NETSCAPE2.0", 11);
HRESULT hr = encoderMetadataWriter->SetMetadataByName(L"/appext/application", &propValue);
if (SUCCEEDED(hr))
{
OutputDebugStringW(L"Encoder application extension set successfully\n");
}
else
{
OutputDebugStringW(L"Failed to set encoder application extension\n");
}
PropVariantClear(&propValue);
// Set loop count (0 = infinite)
PropVariantInit(&propValue);
propValue.vt = VT_UI1 | VT_VECTOR;
propValue.caub.cElems = 5;
propValue.caub.pElems = static_cast<UCHAR*>(CoTaskMemAlloc(5));
if (propValue.caub.pElems != nullptr)
{
propValue.caub.pElems[0] = 3;
propValue.caub.pElems[1] = 1;
propValue.caub.pElems[2] = 0;
propValue.caub.pElems[3] = 0;
propValue.caub.pElems[4] = 0;
hr = encoderMetadataWriter->SetMetadataByName(L"/appext/data", &propValue);
if (SUCCEEDED(hr))
{
OutputDebugStringW(L"Encoder loop count set successfully\n");
}
else
{
OutputDebugStringW(L"Failed to set encoder loop count\n");
}
PropVariantClear(&propValue);
}
}
}
else
{
OutputDebugStringW(L"Failed to get encoder metadata writer\n");
}
}
catch (...)
{
OutputDebugStringW(L"Warning: Failed to set GIF encoder looping metadata\n");
}
}
//----------------------------------------------------------------------------
//
// GifRecordingSession::~GifRecordingSession
//
//----------------------------------------------------------------------------
GifRecordingSession::~GifRecordingSession()
{
Close();
}
//----------------------------------------------------------------------------
//
// GifRecordingSession::Create
//
//----------------------------------------------------------------------------
std::shared_ptr<GifRecordingSession> GifRecordingSession::Create(
winrt::IDirect3DDevice const& device,
winrt::GraphicsCaptureItem const& item,
RECT const& crop,
uint32_t frameRate,
winrt::Streams::IRandomAccessStream const& stream)
{
return std::shared_ptr<GifRecordingSession>(new GifRecordingSession(device, item, crop, frameRate, stream));
}
//----------------------------------------------------------------------------
//
// GifRecordingSession::EncodeFrame
//
//----------------------------------------------------------------------------
HRESULT GifRecordingSession::EncodeFrame(ID3D11Texture2D* frameTexture)
{
try
{
// Create a staging texture for CPU access
D3D11_TEXTURE2D_DESC frameDesc;
frameTexture->GetDesc(&frameDesc);
// GIF encoding with palette generation is VERY slow at high resolutions (4K takes 1 second per frame!)
UINT targetWidth = frameDesc.Width;
UINT targetHeight = frameDesc.Height;
if (frameDesc.Width > static_cast<uint32_t>(m_width) || frameDesc.Height > static_cast<uint32_t>(m_height))
{
float scaleX = static_cast<float>(m_width) / frameDesc.Width;
float scaleY = static_cast<float>(m_height) / frameDesc.Height;
float scale = min(scaleX, scaleY);
targetWidth = static_cast<UINT>(frameDesc.Width * scale);
targetHeight = static_cast<UINT>(frameDesc.Height * scale);
// Ensure even dimensions for GIF
targetWidth = (targetWidth / 2) * 2;
targetHeight = (targetHeight / 2) * 2;
}
D3D11_TEXTURE2D_DESC stagingDesc = frameDesc;
stagingDesc.Usage = D3D11_USAGE_STAGING;
stagingDesc.BindFlags = 0;
stagingDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
stagingDesc.MiscFlags = 0;
winrt::com_ptr<ID3D11Texture2D> stagingTexture;
winrt::check_hresult(m_d3dDevice->CreateTexture2D(&stagingDesc, nullptr, stagingTexture.put()));
// Copy the frame to staging texture
m_d3dContext->CopyResource(stagingTexture.get(), frameTexture);
// Map the staging texture
D3D11_MAPPED_SUBRESOURCE mappedResource;
winrt::check_hresult(m_d3dContext->Map(stagingTexture.get(), 0, D3D11_MAP_READ, 0, &mappedResource));
// Create a new frame in the GIF
winrt::com_ptr<IWICBitmapFrameEncode> frameEncode;
winrt::com_ptr<IPropertyBag2> propertyBag;
winrt::check_hresult(m_gifEncoder->CreateNewFrame(frameEncode.put(), propertyBag.put()));
// Initialize the frame encoder with property bag
winrt::check_hresult(frameEncode->Initialize(propertyBag.get()));
// CRITICAL: For GIF, we MUST set size and pixel format BEFORE WriteSource
// Use target dimensions (may be downsampled)
winrt::check_hresult(frameEncode->SetSize(targetWidth, targetHeight));
// Set the pixel format to 8-bit indexed (required for GIF)
WICPixelFormatGUID pixelFormat = GUID_WICPixelFormat8bppIndexed;
winrt::check_hresult(frameEncode->SetPixelFormat(&pixelFormat));
// Create a WIC bitmap from the BGRA texture data
winrt::com_ptr<IWICBitmap> sourceBitmap;
winrt::check_hresult(m_wicFactory->CreateBitmapFromMemory(
frameDesc.Width,
frameDesc.Height,
GUID_WICPixelFormat32bppBGRA,
mappedResource.RowPitch,
frameDesc.Height * mappedResource.RowPitch,
static_cast<BYTE*>(mappedResource.pData),
sourceBitmap.put()));
// If we need downsampling, use WIC scaler
winrt::com_ptr<IWICBitmapSource> finalSource = sourceBitmap;
if (targetWidth != frameDesc.Width || targetHeight != frameDesc.Height)
{
winrt::com_ptr<IWICBitmapScaler> scaler;
winrt::check_hresult(m_wicFactory->CreateBitmapScaler(scaler.put()));
winrt::check_hresult(scaler->Initialize(
sourceBitmap.get(),
targetWidth,
targetHeight,
WICBitmapInterpolationModeHighQualityCubic));
finalSource = scaler;
OutputDebugStringW((L"Downsampled from " + std::to_wstring(frameDesc.Width) + L"x" + std::to_wstring(frameDesc.Height) +
L" to " + std::to_wstring(targetWidth) + L"x" + std::to_wstring(targetHeight) + L"\n").c_str());
}
// Use WriteSource - WIC will handle the BGRA to 8bpp indexed conversion
winrt::check_hresult(frameEncode->WriteSource(finalSource.get(), nullptr));
try
{
winrt::com_ptr<IWICMetadataQueryWriter> frameMetadataWriter;
if (SUCCEEDED(frameEncode->GetMetadataQueryWriter(frameMetadataWriter.put())) && frameMetadataWriter)
{
// Set the frame delay in the metadata (in hundredths of a second)
PROPVARIANT propValue;
PropVariantInit(&propValue);
propValue.vt = VT_UI2;
propValue.uiVal = static_cast<USHORT>(m_frameDelay);
frameMetadataWriter->SetMetadataByName(L"/grctlext/Delay", &propValue);
PropVariantClear(&propValue);
// Set disposal method (2 = restore to background, needed for animation)
PropVariantInit(&propValue);
propValue.vt = VT_UI1;
propValue.bVal = 2; // Disposal method: restore to background color
frameMetadataWriter->SetMetadataByName(L"/grctlext/Disposal", &propValue);
PropVariantClear(&propValue);
}
}
catch (...)
{
// Metadata setting failed, continue anyway
OutputDebugStringW(L"Warning: Failed to set GIF frame metadata\n");
}
// Commit the frame
OutputDebugStringW(L"About to commit frame to encoder...\n");
winrt::check_hresult(frameEncode->Commit());
OutputDebugStringW(L"Frame committed successfully\n");
// Unmap the staging texture
m_d3dContext->Unmap(stagingTexture.get(), 0);
// Increment and log frame count
m_frameCount++;
OutputDebugStringW((L"GIF Frame #" + std::to_wstring(m_frameCount) + L" fully encoded and committed\n").c_str());
return S_OK;
}
catch (const winrt::hresult_error& error)
{
OutputDebugStringW(error.message().c_str());
return error.code();
}
}
//----------------------------------------------------------------------------
//
// GifRecordingSession::StartAsync
//
//----------------------------------------------------------------------------
winrt::IAsyncAction GifRecordingSession::StartAsync()
{
auto expected = false;
if (m_isRecording.compare_exchange_strong(expected, true))
{
auto self = shared_from_this();
try
{
// Start capturing frames
auto frameStartTime = std::chrono::high_resolution_clock::now();
int captureAttempts = 0;
int successfulCaptures = 0;
int duplicatedFrames = 0;
// Keep track of the last frame to duplicate when needed
winrt::com_ptr<ID3D11Texture2D> lastCroppedTexture;
while (m_isRecording && !m_closed)
{
captureAttempts++;
auto frame = m_frameWait->TryGetNextFrame();
winrt::com_ptr<ID3D11Texture2D> croppedTexture;
if (frame)
{
successfulCaptures++;
auto contentSize = frame->ContentSize;
auto frameTexture = GetDXGIInterfaceFromObject<ID3D11Texture2D>(frame->FrameTexture);
D3D11_TEXTURE2D_DESC desc = {};
frameTexture->GetDesc(&desc);
// Use the smaller of the crop size or content size
auto width = min(m_rcCrop.right - m_rcCrop.left, contentSize.Width);
auto height = min(m_rcCrop.bottom - m_rcCrop.top, contentSize.Height);
D3D11_TEXTURE2D_DESC croppedDesc = {};
croppedDesc.Width = width;
croppedDesc.Height = height;
croppedDesc.MipLevels = 1;
croppedDesc.ArraySize = 1;
croppedDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
croppedDesc.SampleDesc.Count = 1;
croppedDesc.Usage = D3D11_USAGE_DEFAULT;
croppedDesc.BindFlags = D3D11_BIND_RENDER_TARGET;
winrt::check_hresult(m_d3dDevice->CreateTexture2D(&croppedDesc, nullptr, croppedTexture.put()));
// Set the content region to copy and clamp the coordinates
D3D11_BOX region = {};
region.left = std::clamp(m_rcCrop.left, static_cast<LONG>(0), static_cast<LONG>(desc.Width));
region.right = std::clamp(m_rcCrop.left + width, static_cast<LONG>(0), static_cast<LONG>(desc.Width));
region.top = std::clamp(m_rcCrop.top, static_cast<LONG>(0), static_cast<LONG>(desc.Height));
region.bottom = std::clamp(m_rcCrop.top + height, static_cast<LONG>(0), static_cast<LONG>(desc.Height));
region.back = 1;
// Copy the cropped region
m_d3dContext->CopySubresourceRegion(
croppedTexture.get(),
0,
0, 0, 0,
frameTexture.get(),
0,
&region);
// Save this as the last frame for duplication
lastCroppedTexture = croppedTexture;
}
else if (lastCroppedTexture)
{
// No new frame, duplicate the last one
duplicatedFrames++;
croppedTexture = lastCroppedTexture;
}
// Encode the frame (either new or duplicated)
if (croppedTexture)
{
HRESULT hr = EncodeFrame(croppedTexture.get());
if (FAILED(hr))
{
CloseInternal();
break;
}
}
// Wait for the next frame interval
co_await winrt::resume_after(std::chrono::milliseconds(1000 / m_frameRate));
}
// Commit the GIF encoder
if (m_gifEncoder)
{
auto frameEndTime = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(frameEndTime - frameStartTime).count();
OutputDebugStringW(L"Recording stopped. Committing GIF encoder...\n");
OutputDebugStringW((L"Total frames captured: " + std::to_wstring(m_frameCount) + L"\n").c_str());
OutputDebugStringW((L"Capture attempts: " + std::to_wstring(captureAttempts) + L"\n").c_str());
OutputDebugStringW((L"Successful captures: " + std::to_wstring(successfulCaptures) + L"\n").c_str());
OutputDebugStringW((L"Duplicated frames: " + std::to_wstring(duplicatedFrames) + L"\n").c_str());
OutputDebugStringW((L"Recording duration: " + std::to_wstring(duration) + L"ms\n").c_str());
OutputDebugStringW((L"Actual FPS: " + std::to_wstring(m_frameCount * 1000.0 / duration) + L"\n").c_str());
winrt::check_hresult(m_gifEncoder->Commit());
OutputDebugStringW(L"GIF encoder committed successfully\n");
}
}
catch (const winrt::hresult_error& error)
{
OutputDebugStringW(L"Error in GIF recording: ");
OutputDebugStringW(error.message().c_str());
OutputDebugStringW(L"\n");
// Try to commit the encoder even on error
if (m_gifEncoder)
{
try
{
m_gifEncoder->Commit();
}
catch (...) {}
}
CloseInternal();
}
}
co_return;
}
//----------------------------------------------------------------------------
//
// GifRecordingSession::Close
//
//----------------------------------------------------------------------------
void GifRecordingSession::Close()
{
auto expected = false;
if (m_closed.compare_exchange_strong(expected, true))
{
expected = true;
if (!m_isRecording.compare_exchange_strong(expected, false))
{
CloseInternal();
}
else
{
m_frameWait->StopCapture();
}
}
}
//----------------------------------------------------------------------------
//
// GifRecordingSession::CloseInternal
//
//----------------------------------------------------------------------------
void GifRecordingSession::CloseInternal()
{
m_frameWait->StopCapture();
m_itemClosed.revoke();
}

View File

@@ -0,0 +1,69 @@
//==============================================================================
//
// Zoomit
// Sysinternals - www.sysinternals.com
//
// GIF recording support using Windows Imaging Component (WIC)
//
//==============================================================================
#pragma once
#include "CaptureFrameWait.h"
#include <d3d11_4.h>
#include <vector>
class GifRecordingSession : public std::enable_shared_from_this<GifRecordingSession>
{
public:
[[nodiscard]] static std::shared_ptr<GifRecordingSession> Create(
winrt::Direct3D11::IDirect3DDevice const& device,
winrt::GraphicsCaptureItem const& item,
RECT const& cropRect,
uint32_t frameRate,
winrt::Streams::IRandomAccessStream const& stream);
~GifRecordingSession();
winrt::IAsyncAction StartAsync();
void EnableCursorCapture(bool enable = true) { m_frameWait->EnableCursorCapture(enable); }
void Close();
private:
GifRecordingSession(
winrt::Direct3D11::IDirect3DDevice const& device,
winrt::Capture::GraphicsCaptureItem const& item,
RECT const cropRect,
uint32_t frameRate,
winrt::Streams::IRandomAccessStream const& stream);
void CloseInternal();
HRESULT EncodeFrame(ID3D11Texture2D* texture);
private:
winrt::Direct3D11::IDirect3DDevice m_device{ nullptr };
winrt::com_ptr<ID3D11Device> m_d3dDevice;
winrt::com_ptr<ID3D11DeviceContext> m_d3dContext;
RECT m_rcCrop;
uint32_t m_frameRate;
winrt::GraphicsCaptureItem m_item{ nullptr };
winrt::GraphicsCaptureItem::Closed_revoker m_itemClosed;
std::shared_ptr<CaptureFrameWait> m_frameWait;
winrt::Streams::IRandomAccessStream m_stream{ nullptr };
// WIC components for GIF encoding
winrt::com_ptr<IWICImagingFactory> m_wicFactory;
winrt::com_ptr<IWICStream> m_wicStream;
winrt::com_ptr<IWICBitmapEncoder> m_gifEncoder;
winrt::com_ptr<IWICMetadataQueryWriter> m_encoderMetadataWriter;
std::atomic<bool> m_isRecording = false;
std::atomic<bool> m_closed = false;
uint32_t m_frameWidth=0;
uint32_t m_frameHeight=0;
uint32_t m_frameDelay=0;
uint32_t m_frameCount = 0;
int32_t m_width=0;
int32_t m_height=0;
};

View File

@@ -32,18 +32,18 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
// TEXTINCLUDE // TEXTINCLUDE
// //
1 TEXTINCLUDE 1 TEXTINCLUDE
BEGIN BEGIN
"resource.h\0" "resource.h\0"
END END
2 TEXTINCLUDE 2 TEXTINCLUDE
BEGIN BEGIN
"#include ""winres.h""\r\n" "#include ""winres.h""\r\n"
"\0" "\0"
END END
3 TEXTINCLUDE 3 TEXTINCLUDE
BEGIN BEGIN
"#include ""binres.rc""\0" "#include ""binres.rc""\0"
END END
@@ -121,8 +121,8 @@ FONT 8, "MS Shell Dlg", 0, 0, 0x0
BEGIN BEGIN
DEFPUSHBUTTON "OK",IDOK,166,306,50,14 DEFPUSHBUTTON "OK",IDOK,166,306,50,14
PUSHBUTTON "Cancel",IDCANCEL,223,306,50,14 PUSHBUTTON "Cancel",IDCANCEL,223,306,50,14
LTEXT "ZoomIt v9.10",IDC_VERSION,42,7,73,10 LTEXT "ZoomIt v9.20",IDC_VERSION,42,7,73,10
LTEXT "Copyright © 2006-2025 Mark Russinovich",IDC_COPYRIGHT,42,17,231,8 LTEXT "Copyright <EFBFBD> 2006-2025 Mark Russinovich",IDC_COPYRIGHT,42,17,231,8
CONTROL "<a HREF=""https://www.sysinternals.com"">Sysinternals - www.sysinternals.com</a>",IDC_LINK, CONTROL "<a HREF=""https://www.sysinternals.com"">Sysinternals - www.sysinternals.com</a>",IDC_LINK,
"SysLink",WS_TABSTOP,42,26,150,9 "SysLink",WS_TABSTOP,42,26,150,9
ICON "APPICON",IDC_STATIC,12,9,20,20 ICON "APPICON",IDC_STATIC,12,9,20,20
@@ -272,13 +272,15 @@ BEGIN
LTEXT "Note: Recording is only available on Windows 10 (version 1903) and higher.",IDC_STATIC,7,77,246,19 LTEXT "Note: Recording is only available on Windows 10 (version 1903) and higher.",IDC_STATIC,7,77,246,19
LTEXT "Scaling:",IDC_STATIC,30,115,26,8 LTEXT "Scaling:",IDC_STATIC,30,115,26,8
COMBOBOX IDC_RECORD_SCALING,61,114,26,30,CBS_DROPDOWNLIST | CBS_AUTOHSCROLL | CBS_OEMCONVERT | CBS_SORT | WS_VSCROLL | WS_TABSTOP COMBOBOX IDC_RECORD_SCALING,61,114,26,30,CBS_DROPDOWNLIST | CBS_AUTOHSCROLL | CBS_OEMCONVERT | CBS_SORT | WS_VSCROLL | WS_TABSTOP
LTEXT "Format:",IDC_STATIC,30,132,26,8
COMBOBOX IDC_RECORD_FORMAT,61,131,60,30,CBS_DROPDOWNLIST | CBS_AUTOHSCROLL | CBS_OEMCONVERT | WS_VSCROLL | WS_TABSTOP
LTEXT "Frame Rate:",IDC_STATIC,119,115,44,8,NOT WS_VISIBLE LTEXT "Frame Rate:",IDC_STATIC,119,115,44,8,NOT WS_VISIBLE
COMBOBOX IDC_RECORD_FRAME_RATE,166,114,42,30,CBS_DROPDOWNLIST | CBS_AUTOHSCROLL | CBS_OEMCONVERT | CBS_SORT | NOT WS_VISIBLE | WS_VSCROLL | WS_TABSTOP COMBOBOX IDC_RECORD_FRAME_RATE,166,114,42,30,CBS_DROPDOWNLIST | CBS_AUTOHSCROLL | CBS_OEMCONVERT | CBS_SORT | NOT WS_VISIBLE | WS_VSCROLL | WS_TABSTOP
LTEXT "To crop the portion of the screen that will be recorded, enter the hotkey with the Shift key in the opposite mode. ",IDC_STATIC,7,32,246,19 LTEXT "To crop the portion of the screen that will be recorded, enter the hotkey with the Shift key in the opposite mode. ",IDC_STATIC,7,32,246,19
LTEXT "To record a specific window, enter the hotkey with the Alt key in the opposite mode.",IDC_STATIC,7,55,246,19 LTEXT "To record a specific window, enter the hotkey with the Alt key in the opposite mode.",IDC_STATIC,7,55,246,19
CONTROL "&Capture audio input:",IDC_CAPTURE_AUDIO,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,137,83,10 CONTROL "&Capture audio input:",IDC_CAPTURE_AUDIO,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,149,83,10
COMBOBOX IDC_MICROPHONE,81,152,172,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP COMBOBOX IDC_MICROPHONE,81,164,172,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
LTEXT "Microphone:",IDC_STATIC,32,154,47,8 LTEXT "Microphone:",IDC_STATIC,32,166,47,8
END END
SNIP DIALOGEX 0, 0, 260, 68 SNIP DIALOGEX 0, 0, 260, 68

View File

@@ -234,6 +234,7 @@
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile> </ClCompile>
<ClCompile Include="GifRecordingSession.cpp" />
<ClCompile Include="pch.cpp" /> <ClCompile Include="pch.cpp" />
<ClCompile Include="SelectRectangle.cpp"> <ClCompile Include="SelectRectangle.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Use</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Use</PrecompiledHeader>
@@ -288,6 +289,7 @@
<ClInclude Include="AudioSampleGenerator.h" /> <ClInclude Include="AudioSampleGenerator.h" />
<ClInclude Include="$(MSBuildThisFileDirectory)..\..\..\common\sysinternals\Eula\Eula.h" /> <ClInclude Include="$(MSBuildThisFileDirectory)..\..\..\common\sysinternals\Eula\Eula.h" />
<ClInclude Include="$(MSBuildThisFileDirectory)..\ZoomItModuleInterface\Trace.h" /> <ClInclude Include="$(MSBuildThisFileDirectory)..\ZoomItModuleInterface\Trace.h" />
<ClInclude Include="GifRecordingSession.h" />
<ClInclude Include="pch.h" /> <ClInclude Include="pch.h" />
<ClInclude Include="Registry.h" /> <ClInclude Include="Registry.h" />
<ClInclude Include="resource.h" /> <ClInclude Include="resource.h" />

View File

@@ -54,6 +54,9 @@
<ClCompile Include="$(MSBuildThisFileDirectory)..\..\..\common\sysinternals\WindowsVersions.cpp"> <ClCompile Include="$(MSBuildThisFileDirectory)..\..\..\common\sysinternals\WindowsVersions.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="GifRecordingSession.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="Registry.h"> <ClInclude Include="Registry.h">
@@ -95,6 +98,9 @@
<ClInclude Include="ZoomItSettings.h"> <ClInclude Include="ZoomItSettings.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="GifRecordingSession.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Image Include="appicon.ico"> <Image Include="appicon.ico">

View File

@@ -3,6 +3,13 @@
#include "Registry.h" #include "Registry.h"
#include "DemoType.h" #include "DemoType.h"
// Recording format enum
enum class RecordingFormat
{
GIF = 0,
MP4 = 1
};
DWORD g_ToggleKey = (HOTKEYF_CONTROL << 8)| '1'; DWORD g_ToggleKey = (HOTKEYF_CONTROL << 8)| '1';
DWORD g_LiveZoomToggleKey = ((HOTKEYF_CONTROL) << 8)| '4'; DWORD g_LiveZoomToggleKey = ((HOTKEYF_CONTROL) << 8)| '4';
DWORD g_DrawToggleKey = ((HOTKEYF_CONTROL) << 8)| '2'; DWORD g_DrawToggleKey = ((HOTKEYF_CONTROL) << 8)| '2';
@@ -38,8 +45,10 @@ BOOLEAN g_DemoTypeUserDriven = false;
TCHAR g_DemoTypeFile[MAX_PATH] = {0}; TCHAR g_DemoTypeFile[MAX_PATH] = {0};
DWORD g_DemoTypeSpeedSlider = static_cast<int>(((MIN_TYPING_SPEED - MAX_TYPING_SPEED) / 2) + MAX_TYPING_SPEED); DWORD g_DemoTypeSpeedSlider = static_cast<int>(((MIN_TYPING_SPEED - MAX_TYPING_SPEED) / 2) + MAX_TYPING_SPEED);
DWORD g_RecordFrameRate = 30; DWORD g_RecordFrameRate = 30;
// Divide by 100 to get actual scaling DWORD g_RecordScaling = 100;
DWORD g_RecordScaling = 100; DWORD g_RecordScalingGIF = 50;
DWORD g_RecordScalingMP4 = 100;
RecordingFormat g_RecordingFormat = RecordingFormat::GIF;
BOOLEAN g_CaptureAudio = FALSE; BOOLEAN g_CaptureAudio = FALSE;
TCHAR g_MicrophoneDeviceId[MAX_PATH] = {0}; TCHAR g_MicrophoneDeviceId[MAX_PATH] = {0};
@@ -79,7 +88,9 @@ REG_SETTING RegSettings[] = {
{ L"ZoominSliderLevel", SETTING_TYPE_DWORD, 0, &g_SliderZoomLevel, static_cast<DOUBLE>(g_SliderZoomLevel) }, { L"ZoominSliderLevel", SETTING_TYPE_DWORD, 0, &g_SliderZoomLevel, static_cast<DOUBLE>(g_SliderZoomLevel) },
{ L"Font", SETTING_TYPE_BINARY, sizeof g_LogFont, &g_LogFont, static_cast<DOUBLE>(0) }, { L"Font", SETTING_TYPE_BINARY, sizeof g_LogFont, &g_LogFont, static_cast<DOUBLE>(0) },
{ L"RecordFrameRate", SETTING_TYPE_DWORD, 0, &g_RecordFrameRate, static_cast<DOUBLE>(g_RecordFrameRate) }, { L"RecordFrameRate", SETTING_TYPE_DWORD, 0, &g_RecordFrameRate, static_cast<DOUBLE>(g_RecordFrameRate) },
{ L"RecordScaling", SETTING_TYPE_DWORD, 0, &g_RecordScaling, static_cast<DOUBLE>(g_RecordScaling) }, { L"RecordingFormat", SETTING_TYPE_DWORD, 0, &g_RecordingFormat, static_cast<DOUBLE>(0) },
{ L"RecordScalingGIF", SETTING_TYPE_DWORD, 0, &g_RecordScalingGIF, static_cast<DOUBLE>(g_RecordScalingGIF) },
{ L"RecordScalingMP4", SETTING_TYPE_DWORD, 0, &g_RecordScalingMP4, static_cast<DOUBLE>(g_RecordScalingMP4) },
{ L"CaptureAudio", SETTING_TYPE_BOOLEAN, 0, &g_CaptureAudio, static_cast<DOUBLE>(g_CaptureAudio) }, { L"CaptureAudio", SETTING_TYPE_BOOLEAN, 0, &g_CaptureAudio, static_cast<DOUBLE>(g_CaptureAudio) },
{ L"MicrophoneDeviceId", SETTING_TYPE_STRING, sizeof(g_MicrophoneDeviceId), g_MicrophoneDeviceId, static_cast<DOUBLE>(0) }, { L"MicrophoneDeviceId", SETTING_TYPE_STRING, sizeof(g_MicrophoneDeviceId), g_MicrophoneDeviceId, static_cast<DOUBLE>(0) },
{ NULL, SETTING_TYPE_DWORD, 0, NULL, static_cast<DOUBLE>(0) } { NULL, SETTING_TYPE_DWORD, 0, NULL, static_cast<DOUBLE>(0) }

File diff suppressed because it is too large Load Diff

View File

@@ -93,6 +93,7 @@
#define IDC_DEMOTYPE_SLIDER2 1074 #define IDC_DEMOTYPE_SLIDER2 1074
#define IDC_DEMOTYPE_STATIC2 1074 #define IDC_DEMOTYPE_STATIC2 1074
#define IDC_COPYRIGHT 1075 #define IDC_COPYRIGHT 1075
#define IDC_RECORD_FORMAT 1076
#define IDC_PEN_WIDTH 1105 #define IDC_PEN_WIDTH 1105
#define IDC_TIMER 1106 #define IDC_TIMER 1106
#define IDC_SMOOTH_IMAGE 1107 #define IDC_SMOOTH_IMAGE 1107

View File

@@ -14,6 +14,9 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation
const unsigned int SPECIAL_SEMANTICS_SHORTCUT = 1; const unsigned int SPECIAL_SEMANTICS_SHORTCUT = 1;
const unsigned int SPECIAL_SEMANTICS_COLOR = 2; const unsigned int SPECIAL_SEMANTICS_COLOR = 2;
const unsigned int SPECIAL_SEMANTICS_LOG_FONT = 3; const unsigned int SPECIAL_SEMANTICS_LOG_FONT = 3;
const unsigned int SPECIAL_SEMANTICS_RECORDING_FORMAT = 4;
const unsigned int SPECIAL_SEMANTICS_RECORD_SCALING_GIF = 5;
const unsigned int SPECIAL_SEMANTICS_RECORD_SCALING_MP4 = 6;
std::vector<unsigned char> base64_decode(const std::wstring& base64_string) std::vector<unsigned char> base64_decode(const std::wstring& base64_string)
{ {
@@ -72,6 +75,9 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation
{ L"PenColor", SPECIAL_SEMANTICS_COLOR }, { L"PenColor", SPECIAL_SEMANTICS_COLOR },
{ L"BreakPenColor", SPECIAL_SEMANTICS_COLOR }, { L"BreakPenColor", SPECIAL_SEMANTICS_COLOR },
{ L"Font", SPECIAL_SEMANTICS_LOG_FONT }, { L"Font", SPECIAL_SEMANTICS_LOG_FONT },
{ L"RecordingFormat", SPECIAL_SEMANTICS_RECORDING_FORMAT },
{ L"RecordScalingGIF", SPECIAL_SEMANTICS_RECORD_SCALING_GIF },
{ L"RecordScalingMP4", SPECIAL_SEMANTICS_RECORD_SCALING_MP4 },
}; };
hstring ZoomItSettings::LoadSettingsJson() hstring ZoomItSettings::LoadSettingsJson()
@@ -103,6 +109,11 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation
value & 0xFF); value & 0xFF);
_settings.add_property(curSetting->ValueName, hotkey.get_json()); _settings.add_property(curSetting->ValueName, hotkey.get_json());
} }
else if (special_semantics->second == SPECIAL_SEMANTICS_RECORDING_FORMAT)
{
std::wstring formatString = (value == 0) ? L"GIF" : L"MP4";
_settings.add_property(L"RecordFormat", formatString);
}
else if (special_semantics->second == SPECIAL_SEMANTICS_COLOR) else if (special_semantics->second == SPECIAL_SEMANTICS_COLOR)
{ {
/* PowerToys settings likes colors as #FFFFFF strings. /* PowerToys settings likes colors as #FFFFFF strings.
@@ -156,6 +167,9 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation
curSetting++; curSetting++;
} }
DWORD recordScaling = (g_RecordingFormat == static_cast<RecordingFormat>(0)) ? g_RecordScalingGIF : g_RecordScalingMP4;
_settings.add_property<DWORD>(L"RecordScaling", recordScaling);
return _settings.get_raw_json().Stringify(); return _settings.get_raw_json().Stringify();
} }
@@ -167,6 +181,8 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation
PowerToysSettings::PowerToyValues valuesFromSettings = PowerToysSettings::PowerToyValues valuesFromSettings =
PowerToysSettings::PowerToyValues::from_json_string(json, L"ZoomIt"); PowerToysSettings::PowerToyValues::from_json_string(json, L"ZoomIt");
bool formatChanged = false;
PREG_SETTING curSetting = RegSettings; PREG_SETTING curSetting = RegSettings;
while (curSetting->ValueName) while (curSetting->ValueName)
{ {
@@ -212,6 +228,42 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation
*static_cast<PDWORD>(curSetting->Setting) = value; *static_cast<PDWORD>(curSetting->Setting) = value;
} }
} }
else if (special_semantics->second == SPECIAL_SEMANTICS_RECORDING_FORMAT)
{
// Convert string ("GIF" or "MP4") to DWORD enum value (0=GIF, 1=MP4)
auto possibleValue = valuesFromSettings.get_string_value(L"RecordFormat");
if (possibleValue.has_value())
{
RecordingFormat oldFormat = g_RecordingFormat;
DWORD formatValue = (possibleValue.value() == L"GIF") ? 0 : 1;
RecordingFormat newFormat = static_cast<RecordingFormat>(formatValue);
*static_cast<PDWORD>(curSetting->Setting) = formatValue;
if (oldFormat != newFormat)
{
formatChanged = true;
if (oldFormat == static_cast<RecordingFormat>(0))
{
g_RecordScalingGIF = g_RecordScaling;
}
else
{
g_RecordScalingMP4 = g_RecordScaling;
}
if (newFormat == static_cast<RecordingFormat>(0))
{
g_RecordScaling = g_RecordScalingGIF;
}
else
{
g_RecordScaling = g_RecordScalingMP4;
}
}
}
}
else if (special_semantics->second == SPECIAL_SEMANTICS_COLOR) else if (special_semantics->second == SPECIAL_SEMANTICS_COLOR)
{ {
/* PowerToys settings likes colors as #FFFFFF strings. /* PowerToys settings likes colors as #FFFFFF strings.
@@ -275,6 +327,22 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation
} }
curSetting++; curSetting++;
} }
auto recordScalingValue = valuesFromSettings.get_uint_value(L"RecordScaling");
if (recordScalingValue.has_value() && !formatChanged)
{
g_RecordScaling = recordScalingValue.value();
if (g_RecordingFormat == static_cast<RecordingFormat>(0))
{
g_RecordScalingGIF = recordScalingValue.value();
}
else
{
g_RecordScalingMP4 = recordScalingValue.value();
}
}
reg.WriteRegSettings(RegSettings); reg.WriteRegSettings(RegSettings);
} }
} }

View File

@@ -88,9 +88,6 @@ namespace Awake.Core.Native
[return: MarshalAs(UnmanagedType.Bool)] [return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetCursorPos(out Point lpPoint); internal static extern bool GetCursorPos(out Point lpPoint);
[DllImport("user32.dll", SetLastError = true)]
internal static extern bool ScreenToClient(IntPtr hWnd, ref Point lpPoint);
[DllImport("user32.dll", SetLastError = true)] [DllImport("user32.dll", SetLastError = true)]
internal static extern bool GetMessage(out Msg lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax); internal static extern bool GetMessage(out Msg lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);

View File

@@ -61,9 +61,8 @@ namespace Awake.Core
Bridge.SetForegroundWindow(hWnd); Bridge.SetForegroundWindow(hWnd);
// Get cursor position and convert it to client coordinates // Get cursor position in screen coordinates
Bridge.GetCursorPos(out Models.Point cursorPos); Bridge.GetCursorPos(out Models.Point cursorPos);
Bridge.ScreenToClient(hWnd, ref cursorPos);
// Set menu information // Set menu information
MenuInfo menuInfo = new() MenuInfo menuInfo = new()

View File

@@ -51,10 +51,10 @@ internal sealed partial class GlobalErrorHandler
// without its exception being observed. It is NOT raised immediately // without its exception being observed. It is NOT raised immediately
// when the Task faults; timing depends on GC finalization. // when the Task faults; timing depends on GC finalization.
e.SetObserved(); e.SetObserved();
HandleException(e.Exception, Context.UnobservedTaskException, isRecoverable: true); HandleException(e.Exception, Context.UnobservedTaskException);
} }
private void HandleException(Exception ex, Context context, bool isRecoverable = false) private static void HandleException(Exception ex, Context context)
{ {
Logger.LogError($"Unhandled exception detected ({context})", ex); Logger.LogError($"Unhandled exception detected ({context})", ex);
@@ -70,10 +70,25 @@ internal sealed partial class GlobalErrorHandler
StoreReport(report, storeOnDesktop: false); StoreReport(report, storeOnDesktop: false);
string message;
string caption;
try
{
message = ResourceLoaderInstance.GetString("GlobalErrorHandler_CrashMessageBox_Message");
caption = ResourceLoaderInstance.GetString("GlobalErrorHandler_CrashMessageBox_Caption");
}
catch
{
// The resource loader may not be available if the exception occurred during startup.
// Fall back to hardcoded strings in that case.
message = "Command Palette has encountered a fatal error and must close.";
caption = "Command Palette - Fatal error";
}
PInvoke.MessageBox( PInvoke.MessageBox(
HWND.Null, HWND.Null,
"Command Palette has encountered a fatal error and must close.\n\nAn error report has been saved to your desktop.", message,
"Unhandled Error", caption,
MESSAGEBOX_STYLE.MB_ICONERROR); MESSAGEBOX_STYLE.MB_ICONERROR);
} }
} }

View File

@@ -68,7 +68,6 @@ public sealed partial class MainWindow : WindowEx,
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
HideWindow();
_hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32()); _hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32());

View File

@@ -431,8 +431,8 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="Run_Radio_Position_LastPosition.Content" xml:space="preserve"> <data name="Run_Radio_Position_LastPosition.Content" xml:space="preserve">
<value>Last Position</value> <value>Last Position</value>
<comment>Reopen the window where it was last closed</comment> <comment>Reopen the window where it was last closed</comment>
</data> </data>
<data name="TrayMenu_Settings" xml:space="preserve"> <data name="TrayMenu_Settings" xml:space="preserve">
<value>Settings</value> <value>Settings</value>
</data> </data>
<data name="TrayMenu_Close" xml:space="preserve"> <data name="TrayMenu_Close" xml:space="preserve">
@@ -493,28 +493,34 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="Settings_ExtensionsPage_Reloading_Text.Text" xml:space="preserve"> <data name="Settings_ExtensionsPage_Reloading_Text.Text" xml:space="preserve">
<value>Reloading extensions..</value> <value>Reloading extensions..</value>
</data> </data>
<data name="Settings_ExtensionsPage_Banner_Header.Text" xml:space="preserve"> <data name="Settings_ExtensionsPage_Banner_Header.Text" xml:space="preserve">
<value>Discover more extensions</value> <value>Discover more extensions</value>
</data> </data>
<data name="Settings_ExtensionsPage_Banner_Description.Text" xml:space="preserve"> <data name="Settings_ExtensionsPage_Banner_Description.Text" xml:space="preserve">
<value>Find more extensions on the Microsoft Store or WinGet.</value> <value>Find more extensions on the Microsoft Store or WinGet.</value>
</data> </data>
<data name="Settings_ExtensionsPage_Banner_Hyperlink.Content" xml:space="preserve"> <data name="Settings_ExtensionsPage_Banner_Hyperlink.Content" xml:space="preserve">
<value>Learn how to create your own extensions</value> <value>Learn how to create your own extensions</value>
</data> </data>
<data name="Settings_ExtensionsPage_FindExtensions_MicrosoftStore.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve"> <data name="Settings_ExtensionsPage_FindExtensions_MicrosoftStore.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Find extensions on the Microsoft Store</value> <value>Find extensions on the Microsoft Store</value>
</data> </data>
<data name="Settings_ExtensionsPage_FindExtensions_MicrosoftStore.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve"> <data name="Settings_ExtensionsPage_FindExtensions_MicrosoftStore.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Microsoft Store</value> <value>Microsoft Store</value>
</data> </data>
<data name="Settings_ExtensionsPage_FindExtensions_WinGet.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve"> <data name="Settings_ExtensionsPage_FindExtensions_WinGet.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Find extensions on WinGet</value> <value>Find extensions on WinGet</value>
</data> </data>
<data name="Settings_ExtensionsPage_FindExtensions_WinGet.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve"> <data name="Settings_ExtensionsPage_FindExtensions_WinGet.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Microsoft Store</value> <value>Microsoft Store</value>
</data> </data>
<data name="Settings_ExtensionsPage_SearchBox_Placeholder.PlaceholderText" xml:space="preserve"> <data name="Settings_ExtensionsPage_SearchBox_Placeholder.PlaceholderText" xml:space="preserve">
<value>Search extensions</value> <value>Search extensions</value>
</data> </data>
<data name="GlobalErrorHandler_CrashMessageBox_Message" xml:space="preserve">
<value>Command Palette has encountered a fatal error and must close.</value>
</data>
<data name="GlobalErrorHandler_CrashMessageBox_Caption" xml:space="preserve">
<value>Command Palette - Fatal error</value>
</data>
</root> </root>

View File

@@ -5,7 +5,7 @@
<XesUseOneStoreVersioning>true</XesUseOneStoreVersioning> <XesUseOneStoreVersioning>true</XesUseOneStoreVersioning>
<XesBaseYearForStoreVersion>2025</XesBaseYearForStoreVersion> <XesBaseYearForStoreVersion>2025</XesBaseYearForStoreVersion>
<VersionMajor>0</VersionMajor> <VersionMajor>0</VersionMajor>
<VersionMinor>6</VersionMinor> <VersionMinor>7</VersionMinor>
<VersionInfoProductName>Microsoft Command Palette</VersionInfoProductName> <VersionInfoProductName>Microsoft Command Palette</VersionInfoProductName>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@@ -15,8 +15,6 @@ namespace Microsoft.CmdPal.Ext.Apps.Programs;
public sealed partial class AppListItem : ListItem public sealed partial class AppListItem : ListItem
{ {
private static readonly Tag _appTag = new("App");
private readonly AppCommand _appCommand; private readonly AppCommand _appCommand;
private readonly AppItem _app; private readonly AppItem _app;
private readonly Lazy<Details> _details; private readonly Lazy<Details> _details;
@@ -48,7 +46,6 @@ public sealed partial class AppListItem : ListItem
_app = app; _app = app;
Title = app.Name; Title = app.Name;
Subtitle = app.Subtitle; Subtitle = app.Subtitle;
Tags = [_appTag];
Icon = Icons.GenericAppIcon; Icon = Icons.GenericAppIcon;
MoreCommands = AddPinCommands(_app.Commands!, isPinned); MoreCommands = AddPinCommands(_app.Commands!, isPinned);

View File

@@ -63,7 +63,7 @@ internal sealed partial class SampleListPageWithDetails : ListPage
Details = new Details() Details = new Details()
{ {
Title = "Hero Image Example", Title = "Hero Image Example",
HeroImage = new IconInfo("https://m.media-amazon.com/images/M/MV5BNDBkMzVmNGQtYTM2OC00OWRjLTk5OWMtNzNkMDI4NjFjNTZmXkEyXkFqcGdeQXZ3ZXNsZXk@._V1_QL75_UX500_CR0,0,500,281_.jpg"), HeroImage = new IconInfo("https://m.media-amazon.com/images/M/MV5BNDBkMzVmNGQtYTM2OC00OWRjLTk5OWMtNzNkMDI4NjFjNTZmXkEyXkFqcGdeQXZ3ZXNsZXk@._V1_QL75_UX500_CR0,0,500,281_.jpg"), /* #no-spell-check-line */
Body = "It is literally an image of a hero", Body = "It is literally an image of a hero",
}, },
}, },

View File

@@ -10,7 +10,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using ImageResizer.Properties;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq; using Moq;
using Moq.Protected; using Moq.Protected;
@@ -101,7 +101,9 @@ namespace ImageResizer.Models
private static ResizeBatch CreateBatch(Action<string> executeAction) private static ResizeBatch CreateBatch(Action<string> executeAction)
{ {
var mock = new Mock<ResizeBatch> { CallBase = true }; var mock = new Mock<ResizeBatch> { CallBase = true };
mock.Protected().Setup("Execute", ItExpr.IsAny<string>()).Callback(executeAction); mock.Protected()
.Setup("Execute", ItExpr.IsAny<string>(), ItExpr.IsAny<Settings>())
.Callback((string file, Settings settings) => executeAction(file));
return mock.Object; return mock.Object;
} }

View File

@@ -87,9 +87,14 @@ namespace ImageResizer.Models
public IEnumerable<ResizeError> Process(Action<int, double> reportProgress, CancellationToken cancellationToken) public IEnumerable<ResizeError> Process(Action<int, double> reportProgress, CancellationToken cancellationToken)
{ {
double total = Files.Count; double total = Files.Count;
var completed = 0; int completed = 0;
var errors = new ConcurrentBag<ResizeError>(); var errors = new ConcurrentBag<ResizeError>();
// NOTE: Settings.Default is captured once before parallel processing.
// Any changes to settings on disk during this batch will NOT be reflected until the next batch.
// This improves performance and predictability by avoiding repeated mutex acquisition and behaviour change results in a batch.
var settings = Settings.Default;
// TODO: If we ever switch to Windows.Graphics.Imaging, we can get a lot more throughput by using the async // TODO: If we ever switch to Windows.Graphics.Imaging, we can get a lot more throughput by using the async
// APIs and a custom SynchronizationContext // APIs and a custom SynchronizationContext
Parallel.ForEach( Parallel.ForEach(
@@ -97,13 +102,12 @@ namespace ImageResizer.Models
new ParallelOptions new ParallelOptions
{ {
CancellationToken = cancellationToken, CancellationToken = cancellationToken,
MaxDegreeOfParallelism = Environment.ProcessorCount,
}, },
(file, state, i) => (file, state, i) =>
{ {
try try
{ {
Execute(file); Execute(file, settings);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -111,14 +115,13 @@ namespace ImageResizer.Models
} }
Interlocked.Increment(ref completed); Interlocked.Increment(ref completed);
reportProgress(completed, total); reportProgress(completed, total);
}); });
return errors; return errors;
} }
protected virtual void Execute(string file) protected virtual void Execute(string file, Settings settings)
=> new ResizeOperation(file, DestinationDirectory, Settings.Default).Execute(); => new ResizeOperation(file, DestinationDirectory, settings).Execute();
} }
} }

View File

@@ -461,33 +461,42 @@ namespace ImageResizer.Properties
{ {
} }
// Needs to be called on the App UI thread as the properties are bound to the UI. if (App.Current?.Dispatcher != null)
App.Current.Dispatcher.Invoke(() =>
{ {
ShrinkOnly = jsonSettings.ShrinkOnly; // Needs to be called on the App UI thread as the properties are bound to the UI.
Replace = jsonSettings.Replace; App.Current.Dispatcher.Invoke(() => ReloadCore(jsonSettings));
IgnoreOrientation = jsonSettings.IgnoreOrientation; }
RemoveMetadata = jsonSettings.RemoveMetadata; else
JpegQualityLevel = jsonSettings.JpegQualityLevel; {
PngInterlaceOption = jsonSettings.PngInterlaceOption; ReloadCore(jsonSettings);
TiffCompressOption = jsonSettings.TiffCompressOption; }
FileName = jsonSettings.FileName;
KeepDateModified = jsonSettings.KeepDateModified;
FallbackEncoder = jsonSettings.FallbackEncoder;
CustomSize = jsonSettings.CustomSize;
SelectedSizeIndex = jsonSettings.SelectedSizeIndex;
if (jsonSettings.Sizes.Count > 0)
{
Sizes.Clear();
Sizes.AddRange(jsonSettings.Sizes);
// Ensure Ids are unique and handle missing Ids
IdRecoveryHelper.RecoverInvalidIds(Sizes);
}
});
_jsonMutex.ReleaseMutex(); _jsonMutex.ReleaseMutex();
} }
private void ReloadCore(Settings jsonSettings)
{
ShrinkOnly = jsonSettings.ShrinkOnly;
Replace = jsonSettings.Replace;
IgnoreOrientation = jsonSettings.IgnoreOrientation;
RemoveMetadata = jsonSettings.RemoveMetadata;
JpegQualityLevel = jsonSettings.JpegQualityLevel;
PngInterlaceOption = jsonSettings.PngInterlaceOption;
TiffCompressOption = jsonSettings.TiffCompressOption;
FileName = jsonSettings.FileName;
KeepDateModified = jsonSettings.KeepDateModified;
FallbackEncoder = jsonSettings.FallbackEncoder;
CustomSize = jsonSettings.CustomSize;
SelectedSizeIndex = jsonSettings.SelectedSizeIndex;
if (jsonSettings.Sizes.Count > 0)
{
Sizes.Clear();
Sizes.AddRange(jsonSettings.Sizes);
// Ensure Ids are unique and handle missing Ids
IdRecoveryHelper.RecoverInvalidIds(Sizes);
}
}
} }
} }

View File

@@ -24,13 +24,15 @@ using Windows.Storage;
namespace Peek.FilePreviewer.Previewers.MediaPreviewer namespace Peek.FilePreviewer.Previewers.MediaPreviewer
{ {
public partial class AudioPreviewer : ObservableObject, IAudioPreviewer public partial class AudioPreviewer : ObservableObject, IDisposable, IAudioPreviewer
{ {
private MediaSource? _mediaSource;
[ObservableProperty] [ObservableProperty]
private PreviewState _state; private PreviewState _state;
[ObservableProperty] [ObservableProperty]
private AudioPreviewData _preview; private AudioPreviewData? _preview;
private IFileSystemItem Item { get; } private IFileSystemItem Item { get; }
@@ -40,7 +42,6 @@ namespace Peek.FilePreviewer.Previewers.MediaPreviewer
{ {
Item = file; Item = file;
Dispatcher = DispatcherQueue.GetForCurrentThread(); Dispatcher = DispatcherQueue.GetForCurrentThread();
Preview = new AudioPreviewData();
} }
public async Task CopyAsync() public async Task CopyAsync()
@@ -63,19 +64,23 @@ namespace Peek.FilePreviewer.Previewers.MediaPreviewer
{ {
State = PreviewState.Loading; State = PreviewState.Loading;
Preview = new AudioPreviewData();
var thumbnailTask = LoadThumbnailAsync(cancellationToken); var thumbnailTask = LoadThumbnailAsync(cancellationToken);
var sourceTask = LoadSourceAsync(cancellationToken); var sourceTask = LoadSourceAsync(cancellationToken);
var metadataTask = LoadMetadataAsync(cancellationToken); var metadataTask = LoadMetadataAsync(cancellationToken);
await Task.WhenAll(thumbnailTask, sourceTask, metadataTask); await Task.WhenAll(thumbnailTask, sourceTask, metadataTask);
if (!thumbnailTask.Result || !sourceTask.Result || !metadataTask.Result) if (sourceTask.Result && metadataTask.Result)
{ {
State = PreviewState.Error; State = PreviewState.Loaded;
} }
else else
{ {
State = PreviewState.Loaded; // Release all resources on error.
Unload();
State = PreviewState.Error;
} }
} }
@@ -88,12 +93,15 @@ namespace Peek.FilePreviewer.Previewers.MediaPreviewer
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
var thumbnail = await ThumbnailHelper.GetThumbnailAsync(Item.Path, cancellationToken) if (Preview != null)
?? await ThumbnailHelper.GetIconAsync(Item.Path, cancellationToken); {
var thumbnail = await ThumbnailHelper.GetThumbnailAsync(Item.Path, cancellationToken)
?? await ThumbnailHelper.GetIconAsync(Item.Path, cancellationToken);
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
Preview.Thumbnail = thumbnail ?? new SvgImageSource(new Uri("ms-appx:///Assets/Peek/DefaultFileIcon.svg")); Preview.Thumbnail = thumbnail ?? new SvgImageSource(new Uri("ms-appx:///Assets/Peek/DefaultFileIcon.svg"));
}
}); });
}); });
} }
@@ -110,7 +118,11 @@ namespace Peek.FilePreviewer.Previewers.MediaPreviewer
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
Preview.MediaSource = MediaSource.CreateFromStorageFile(storageFile); if (Preview != null)
{
_mediaSource = MediaSource.CreateFromStorageFile(storageFile);
Preview.MediaSource = _mediaSource;
}
}); });
}); });
} }
@@ -123,6 +135,11 @@ namespace Peek.FilePreviewer.Previewers.MediaPreviewer
await Dispatcher.RunOnUiThread(() => await Dispatcher.RunOnUiThread(() =>
{ {
if (Preview == null)
{
return;
}
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
Preview.Title = PropertyStoreHelper.TryGetStringProperty(Item.Path, PropertyKey.MusicTitle) Preview.Title = PropertyStoreHelper.TryGetStringProperty(Item.Path, PropertyKey.MusicTitle)
?? Item.Name[..^Item.Extension.Length]; ?? Item.Name[..^Item.Extension.Length];
@@ -160,6 +177,22 @@ namespace Peek.FilePreviewer.Previewers.MediaPreviewer
return _supportedFileTypes.Contains(item.Extension); return _supportedFileTypes.Contains(item.Extension);
} }
public void Dispose()
{
Unload();
GC.SuppressFinalize(this);
}
/// <summary>
/// Explicitly unloads the preview and releases file resources.
/// </summary>
public void Unload()
{
_mediaSource?.Dispose();
_mediaSource = null;
Preview = null;
}
private static readonly HashSet<string> _supportedFileTypes = new() private static readonly HashSet<string> _supportedFileTypes = new()
{ {
".aac", ".aac",

View File

@@ -25,6 +25,8 @@ namespace Peek.FilePreviewer.Previewers
{ {
public partial class VideoPreviewer : ObservableObject, IVideoPreviewer, IDisposable public partial class VideoPreviewer : ObservableObject, IVideoPreviewer, IDisposable
{ {
private MediaSource? _mediaSource;
[ObservableProperty] [ObservableProperty]
private MediaSource? preview; private MediaSource? preview;
@@ -56,6 +58,7 @@ namespace Peek.FilePreviewer.Previewers
public void Dispose() public void Dispose()
{ {
Unload();
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
@@ -145,7 +148,8 @@ namespace Peek.FilePreviewer.Previewers
MissingCodecName = missingCodecName; MissingCodecName = missingCodecName;
} }
Preview = MediaSource.CreateFromStorageFile(storageFile); _mediaSource = MediaSource.CreateFromStorageFile(storageFile);
Preview = _mediaSource;
}); });
}); });
} }
@@ -155,6 +159,16 @@ namespace Peek.FilePreviewer.Previewers
return !(VideoTask?.Result ?? true); return !(VideoTask?.Result ?? true);
} }
/// <summary>
/// Explicitly unloads the preview and releases file resources.
/// </summary>
public void Unload()
{
_mediaSource?.Dispose();
_mediaSource = null;
Preview = null;
}
private static readonly HashSet<string> _supportedFileTypes = new() private static readonly HashSet<string> _supportedFileTypes = new()
{ {
".mp4", ".3g2", ".3gp", ".3gp2", ".3gpp", ".asf", ".avi", ".m2t", ".m2ts", ".mp4", ".3g2", ".3gp", ".3gp2", ".3gpp", ".asf", ".avi", ".m2t", ".m2ts",

View File

@@ -9,6 +9,7 @@ using System.Drawing;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.PowerToys.UITest; using Microsoft.PowerToys.UITest;
@@ -35,6 +36,105 @@ public class PeekFilePreviewTests : UITestBase
{ {
} }
static PeekFilePreviewTests()
{
FixSettingsFileBeforeTests();
}
private static readonly JsonSerializerOptions IndentedJsonOptions = new() { WriteIndented = true };
private static void FixSettingsFileBeforeTests()
{
try
{
// Default Peek settings
string peekSettingsContent = @"{
""name"": ""Peek"",
""version"": ""1.0"",
""properties"": {
""ActivationShortcut"": {
""win"": false,
""ctrl"": true,
""alt"": false,
""shift"": false,
""code"": 32,
""key"": ""Space""
},
""AlwaysRunNotElevated"": {
""value"": true
},
""CloseAfterLosingFocus"": {
""value"": false
},
""ConfirmFileDelete"": {
""value"": true
},
""EnableSpaceToActivate"": {
""value"": false
}
}
}";
// Update Peek module settings
SettingsConfigHelper.UpdateModuleSettings(
"Peek",
peekSettingsContent,
(settings) =>
{
// Get or ensure properties section exists
Dictionary<string, object> properties;
if (settings.TryGetValue("properties", out var propertiesObj))
{
if (propertiesObj is Dictionary<string, object> dict)
{
properties = dict;
}
else if (propertiesObj is JsonElement jsonElem)
{
properties = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonElem.GetRawText())
?? throw new InvalidOperationException("Failed to deserialize properties");
}
else
{
properties = new Dictionary<string, object>();
}
}
else
{
properties = new Dictionary<string, object>();
}
// Update the required properties
properties["ActivationShortcut"] = new Dictionary<string, object>
{
{ "win", false },
{ "ctrl", true },
{ "alt", false },
{ "shift", false },
{ "code", 32 },
{ "key", "Space" },
};
properties["EnableSpaceToActivate"] = new Dictionary<string, object>
{
{ "value", false },
};
settings["properties"] = properties;
});
// Disable all modules except Peek in global settings
SettingsConfigHelper.ConfigureGlobalModuleSettings("Peek");
Debug.WriteLine("Successfully updated all settings - Peek shortcut configured and all modules except Peek disabled");
}
catch (Exception ex)
{
Assert.Fail($"ERROR in FixSettingsFileBeforeTests: {ex.Message}");
}
}
[TestInitialize] [TestInitialize]
public void TestInitialize() public void TestInitialize()
{ {

View File

@@ -212,7 +212,7 @@ namespace PowerAccent.Core
LetterKey.VK_L => new[] { "ļ", "₺" }, // ₺ is in VK_T for other languages, but not VK_L, so we add it here. LetterKey.VK_L => new[] { "ļ", "₺" }, // ₺ is in VK_T for other languages, but not VK_L, so we add it here.
LetterKey.VK_M => new[] { "ṁ" }, LetterKey.VK_M => new[] { "ṁ" },
LetterKey.VK_N => new[] { "ņ", "ṅ", "ⁿ", "", "№" }, LetterKey.VK_N => new[] { "ņ", "ṅ", "ⁿ", "", "№" },
LetterKey.VK_O => new[] { "ȯ", "∅" }, LetterKey.VK_O => new[] { "ȯ", "∅", "⌀" },
LetterKey.VK_P => new[] { "ṗ", "℗", "∏", "¶" }, LetterKey.VK_P => new[] { "ṗ", "℗", "∏", "¶" },
LetterKey.VK_Q => new[] { "" }, LetterKey.VK_Q => new[] { "" },
LetterKey.VK_R => new[] { "ṙ", "®", "" }, LetterKey.VK_R => new[] { "ṙ", "®", "" },

View File

@@ -71,6 +71,7 @@
</ClCompile> </ClCompile>
<Link> <Link>
<SubSystem>Console</SubSystem> <SubSystem>Console</SubSystem>
<AdditionalDependencies>windowscodecs.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link> </Link>
</ItemDefinitionGroup> </ItemDefinitionGroup>
<ItemGroup> <ItemGroup>

View File

@@ -28,6 +28,7 @@
<PlatformToolset>v143</PlatformToolset> <PlatformToolset>v143</PlatformToolset>
<WindowsPackageType>None</WindowsPackageType> <WindowsPackageType>None</WindowsPackageType>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained> <WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<WindowsAppSdkUndockedRegFreeWinRTInitialize>true</WindowsAppSdkUndockedRegFreeWinRTInitialize>
</PropertyGroup> </PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" /> <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings"> <ImportGroup Label="ExtensionSettings">

View File

@@ -56,9 +56,9 @@ public static class AIServiceTypeRegistry
IsOnlineService = true, IsOnlineService = true,
LegalDescription = "AdvancedPaste_Google_LegalDescription", LegalDescription = "AdvancedPaste_Google_LegalDescription",
TermsLabel = "AdvancedPaste_Google_TermsLabel", TermsLabel = "AdvancedPaste_Google_TermsLabel",
TermsUri = new Uri("https://policies.google.com/terms"), TermsUri = new Uri("https://ai.google.dev/gemini-api/terms"),
PrivacyLabel = "AdvancedPaste_Google_PrivacyLabel", PrivacyLabel = "AdvancedPaste_Google_PrivacyLabel",
PrivacyUri = new Uri("https://policies.google.com/privacy"), PrivacyUri = new Uri("https://support.google.com/gemini/answer/13594961"),
}, },
[AIServiceType.Mistral] = new AIServiceTypeMetadata [AIServiceType.Mistral] = new AIServiceTypeMetadata
{ {
@@ -93,9 +93,9 @@ public static class AIServiceTypeRegistry
IsLocalModel = true, IsLocalModel = true,
LegalDescription = "AdvancedPaste_LocalModel_LegalDescription", LegalDescription = "AdvancedPaste_LocalModel_LegalDescription",
TermsLabel = "AdvancedPaste_Ollama_TermsLabel", TermsLabel = "AdvancedPaste_Ollama_TermsLabel",
TermsUri = new Uri("https://ollama.com/terms"), TermsUri = new Uri("https://ollama.org/terms"),
PrivacyLabel = "AdvancedPaste_Ollama_PrivacyLabel", PrivacyLabel = "AdvancedPaste_Ollama_PrivacyLabel",
PrivacyUri = new Uri("https://ollama.com/privacy"), PrivacyUri = new Uri("https://ollama.org/privacy"),
}, },
[AIServiceType.Onnx] = new AIServiceTypeMetadata [AIServiceType.Onnx] = new AIServiceTypeMetadata
{ {

View File

@@ -87,6 +87,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public IntProperty RecordScaling { get; set; } public IntProperty RecordScaling { get; set; }
public StringProperty RecordFormat { get; set; }
public BoolProperty CaptureAudio { get; set; } public BoolProperty CaptureAudio { get; set; }
public StringProperty MicrophoneDeviceId { get; set; } public StringProperty MicrophoneDeviceId { get; set; }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -9,7 +9,10 @@
mc:Ignorable="d"> mc:Ignorable="d">
<Grid> <Grid>
<Button Click="ShortcutConflictBtn_Click" Style="{StaticResource SubtleButtonStyle}"> <Button
x:Uid="ShortcutConflictControl_Automation"
Click="ShortcutConflictBtn_Click"
Style="{StaticResource SubtleButtonStyle}">
<Grid ColumnSpacing="16"> <Grid ColumnSpacing="16">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />

View File

@@ -111,7 +111,8 @@
x:Uid="UpdateAvailableInfoBar" x:Uid="UpdateAvailableInfoBar"
IsClosable="False" IsClosable="False"
IsOpen="{x:Bind ViewModel.IsUpdateAvailable, Mode=OneWay}" IsOpen="{x:Bind ViewModel.IsUpdateAvailable, Mode=OneWay}"
Severity="Success" /> Severity="Success"
Tapped="UpdateInfoBar_Tapped" />
<StackPanel <StackPanel
Grid.Row="1" Grid.Row="1"
@@ -144,7 +145,6 @@
<Button <Button
x:Name="SettingsBtn" x:Name="SettingsBtn"
x:Uid="SettingsBtn" x:Uid="SettingsBtn"
Padding="8"
Click="SettingsBtn_Click" Click="SettingsBtn_Click"
Style="{StaticResource FlyoutButtonStyle}"> Style="{StaticResource FlyoutButtonStyle}">
<ToolTipService.ToolTip> <ToolTipService.ToolTip>

View File

@@ -10,6 +10,7 @@ using Microsoft.PowerToys.Settings.UI.Controls;
using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events; using Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events;
using Microsoft.PowerToys.Settings.UI.ViewModels; using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.PowerToys.Settings.UI.Views;
using Microsoft.PowerToys.Telemetry; using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
@@ -183,5 +184,14 @@ namespace Microsoft.PowerToys.Settings.UI.Flyout
// Closing manually the flyout since no window will steal the focus // Closing manually the flyout since no window will steal the focus
App.GetFlyoutWindow()?.Hide(); App.GetFlyoutWindow()?.Hide();
} }
private void UpdateInfoBar_Tapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e)
{
// Hide the flyout before opening settings window
App.GetFlyoutWindow()?.Hide();
// Open Settings window directly to General page where update controls are located
App.OpenSettingsWindow(typeof(GeneralPage));
}
} }
} }

View File

@@ -240,6 +240,7 @@
<ComboBox <ComboBox
x:Name="Languages_ComboBox" x:Name="Languages_ComboBox"
MinWidth="{StaticResource SettingActionControlMinWidth}" MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.Name="{Binding ElementName=LanguageHeader, Path=Header}"
DisplayMemberPath="Language" DisplayMemberPath="Language"
ItemsSource="{Binding Languages, Mode=TwoWay}" ItemsSource="{Binding Languages, Mode=TwoWay}"
SelectedIndex="{Binding LanguagesIndex, Mode=TwoWay}" /> SelectedIndex="{Binding LanguagesIndex, Mode=TwoWay}" />
@@ -262,7 +263,10 @@
<tkcontrols:SettingsCard.Description> <tkcontrols:SettingsCard.Description>
<HyperlinkButton x:Uid="Windows_Color_Settings" Click="OpenColorsSettings_Click" /> <HyperlinkButton x:Uid="Windows_Color_Settings" Click="OpenColorsSettings_Click" />
</tkcontrols:SettingsCard.Description> </tkcontrols:SettingsCard.Description>
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.ThemeIndex, Mode=TwoWay}"> <ComboBox
MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.Name="{Binding ElementName=ColorModeHeader, Path=Header}"
SelectedIndex="{x:Bind ViewModel.ThemeIndex, Mode=TwoWay}">
<ComboBoxItem x:Uid="Radio_Theme_Dark" /> <ComboBoxItem x:Uid="Radio_Theme_Dark" />
<ComboBoxItem x:Uid="Radio_Theme_Light" /> <ComboBoxItem x:Uid="Radio_Theme_Light" />
<ComboBoxItem x:Uid="Radio_Theme_Default" /> <ComboBoxItem x:Uid="Radio_Theme_Default" />
@@ -273,12 +277,16 @@
Name="GeneralPageRunAtStartUp" Name="GeneralPageRunAtStartUp"
x:Uid="GeneralPage_RunAtStartUp" x:Uid="GeneralPage_RunAtStartUp"
IsEnabled="{x:Bind ViewModel.IsRunAtStartupGPOManaged, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}"> IsEnabled="{x:Bind ViewModel.IsRunAtStartupGPOManaged, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}">
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.Startup, Mode=TwoWay}" /> <ToggleSwitch
x:Uid="ToggleSwitch"
AutomationProperties.Name="{Binding ElementName=GeneralPageRunAtStartUp, Path=Header}"
IsOn="{x:Bind ViewModel.Startup, Mode=TwoWay}" />
</tkcontrols:SettingsCard> </tkcontrols:SettingsCard>
<controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsRunAtStartupGPOManaged, Mode=OneWay}"> <controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsRunAtStartupGPOManaged, Mode=OneWay}">
<tkcontrols:SettingsCard x:Uid="ShowSystemTrayIcon"> <tkcontrols:SettingsCard x:Name="ShowSystemTrayIconCard" x:Uid="ShowSystemTrayIcon">
<ToggleSwitch <ToggleSwitch
x:Uid="ShowSystemTrayIcon_ToggleSwitch" x:Uid="ShowSystemTrayIcon_ToggleSwitch"
AutomationProperties.Name="{Binding ElementName=ShowSystemTrayIconCard, Path=Header}"
IsOn="{x:Bind ViewModel.ShowSysTrayIcon, Mode=TwoWay}" IsOn="{x:Bind ViewModel.ShowSysTrayIcon, Mode=TwoWay}"
Toggled="ShowSystemTrayIcon_Toggled" /> Toggled="ShowSystemTrayIcon_Toggled" />
</tkcontrols:SettingsCard> </tkcontrols:SettingsCard>
@@ -398,12 +406,16 @@
<tkcontrols:SettingsCard.HeaderIcon> <tkcontrols:SettingsCard.HeaderIcon>
<PathIcon Data="M1859 1758q14 23 21 47t7 51q0 40-15 75t-41 61-61 41-75 15H354q-40 0-75-15t-61-41-41-61-15-75q0-27 6-51t21-47l569-992q10-14 10-34V128H640V0h768v128h-128v604q0 19 10 35l569 991zM896 732q0 53-27 99l-331 577h972l-331-577q-27-46-27-99V128H896v604zm799 1188q26 0 44-19t19-45q0-10-2-17t-8-16l-164-287H464l-165 287q-9 15-9 33 0 26 18 45t46 19h1341z" /> <PathIcon Data="M1859 1758q14 23 21 47t7 51q0 40-15 75t-41 61-61 41-75 15H354q-40 0-75-15t-61-41-41-61-15-75q0-27 6-51t21-47l569-992q10-14 10-34V128H640V0h768v128h-128v604q0 19 10 35l569 991zM896 732q0 53-27 99l-331 577h972l-331-577q-27-46-27-99V128H896v604zm799 1188q26 0 44-19t19-45q0-10-2-17t-8-16l-164-287H464l-165 287q-9 15-9 33 0 26 18 45t46 19h1341z" />
</tkcontrols:SettingsCard.HeaderIcon> </tkcontrols:SettingsCard.HeaderIcon>
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.EnableExperimentation, Mode=TwoWay}" /> <ToggleSwitch
x:Uid="ToggleSwitch"
AutomationProperties.Name="{Binding ElementName=GeneralPageEnableExperimentation, Path=Header}"
IsOn="{x:Bind ViewModel.EnableExperimentation, Mode=TwoWay}" />
</tkcontrols:SettingsCard> </tkcontrols:SettingsCard>
</controls:GPOInfoControl> </controls:GPOInfoControl>
</controls:SettingsGroup> </controls:SettingsGroup>
<controls:SettingsGroup x:Uid="General_DiagnosticsAndFeedback"> <controls:SettingsGroup x:Uid="General_DiagnosticsAndFeedback">
<tkcontrols:SettingsExpander <tkcontrols:SettingsExpander
x:Name="GeneralPageEnableDataDiagnostics"
x:Uid="GeneralPage_EnableDataDiagnostics" x:Uid="GeneralPage_EnableDataDiagnostics"
HeaderIcon="{ui:FontIcon Glyph=&#xE9D9;}" HeaderIcon="{ui:FontIcon Glyph=&#xE9D9;}"
IsEnabled="{x:Bind ViewModel.IsDataDiagnosticsGPOManaged, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}" IsEnabled="{x:Bind ViewModel.IsDataDiagnosticsGPOManaged, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}"
@@ -421,10 +433,19 @@
NavigateUri="https://aka.ms/powertoys-data-and-privacy-documentation" /> NavigateUri="https://aka.ms/powertoys-data-and-privacy-documentation" />
</StackPanel> </StackPanel>
</tkcontrols:SettingsExpander.Description> </tkcontrols:SettingsExpander.Description>
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.EnableDataDiagnostics, Mode=TwoWay}" /> <ToggleSwitch
x:Uid="ToggleSwitch"
AutomationProperties.Name="{Binding ElementName=GeneralPageEnableDataDiagnostics, Path=Header}"
IsOn="{x:Bind ViewModel.EnableDataDiagnostics, Mode=TwoWay}" />
<tkcontrols:SettingsExpander.Items> <tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard x:Uid="GeneralPage_EnableViewDiagnosticData" IsEnabled="{x:Bind ViewModel.EnableDataDiagnostics, Mode=TwoWay}"> <tkcontrols:SettingsCard
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.EnableViewDataDiagnostics, Mode=TwoWay}" /> x:Name="GeneralPageEnableViewDiagnosticData"
x:Uid="GeneralPage_EnableViewDiagnosticData"
IsEnabled="{x:Bind ViewModel.EnableDataDiagnostics, Mode=TwoWay}">
<ToggleSwitch
x:Uid="ToggleSwitch"
AutomationProperties.Name="{Binding ElementName=GeneralPageEnableViewDiagnosticData, Path=Header}"
IsOn="{x:Bind ViewModel.EnableViewDataDiagnostics, Mode=TwoWay}" />
<tkcontrols:SettingsCard.Description> <tkcontrols:SettingsCard.Description>
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">
<TextBlock <TextBlock

View File

@@ -23,7 +23,10 @@
Name="NewPlusEnableToggle" Name="NewPlusEnableToggle"
x:Uid="NewPlus_Enable_Toggle" x:Uid="NewPlus_Enable_Toggle"
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/NewPlus.png}"> HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/NewPlus.png}">
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" /> <ToggleSwitch
x:Uid="ToggleSwitch"
AutomationProperties.Name="{Binding ElementName=NewPlusEnableToggle, Path=Header}"
IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" />
</tkcontrols:SettingsCard> </tkcontrols:SettingsCard>
</controls:GPOInfoControl> </controls:GPOInfoControl>
<controls:SettingsGroup x:Uid="NewPlus_Templates" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}"> <controls:SettingsGroup x:Uid="NewPlus_Templates" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
@@ -60,12 +63,18 @@
Name="NewPlusHideFileExtensionToggle" Name="NewPlusHideFileExtensionToggle"
x:Uid="NewPlus_Hide_File_Extension_Toggle" x:Uid="NewPlus_Hide_File_Extension_Toggle"
IsEnabled="{x:Bind ViewModel.IsHideFileExtSettingsCardEnabled, Mode=OneWay}"> IsEnabled="{x:Bind ViewModel.IsHideFileExtSettingsCardEnabled, Mode=OneWay}">
<ToggleSwitch x:Uid="HideFileExtensionToggle" IsOn="{x:Bind ViewModel.HideFileExtension, Mode=TwoWay}" /> <ToggleSwitch
x:Uid="HideFileExtensionToggle"
AutomationProperties.Name="{Binding ElementName=NewPlusHideFileExtensionToggle, Path=Header}"
IsOn="{x:Bind ViewModel.HideFileExtension, Mode=TwoWay}" />
</tkcontrols:SettingsCard> </tkcontrols:SettingsCard>
</controls:GPOInfoControl> </controls:GPOInfoControl>
<tkcontrols:SettingsCard Name="NewPlusHideStartingDigitsToggle" x:Uid="NewPlus_Hide_Starting_Digits_Toggle"> <tkcontrols:SettingsCard Name="NewPlusHideStartingDigitsToggle" x:Uid="NewPlus_Hide_Starting_Digits_Toggle">
<ToggleSwitch x:Uid="HideStartingDigitsToggle" IsOn="{x:Bind ViewModel.HideStartingDigits, Mode=TwoWay}" /> <ToggleSwitch
x:Uid="HideStartingDigitsToggle"
AutomationProperties.Name="{Binding ElementName=NewPlusHideStartingDigitsToggle, Path=Header}"
IsOn="{x:Bind ViewModel.HideStartingDigits, Mode=TwoWay}" />
<tkcontrols:SettingsCard.Description> <tkcontrols:SettingsCard.Description>
<TextBlock x:Uid="NewPlus_Hide_Starting_Digits_Description" /> <TextBlock x:Uid="NewPlus_Hide_Starting_Digits_Description" />
</tkcontrols:SettingsCard.Description> </tkcontrols:SettingsCard.Description>
@@ -79,7 +88,10 @@
x:Uid="NewPlus_Behaviour_Replace_Variables_Toggle" x:Uid="NewPlus_Behaviour_Replace_Variables_Toggle"
IsEnabled="{x:Bind ViewModel.IsReplaceVariablesSettingsCardEnabled, Mode=OneWay}"> IsEnabled="{x:Bind ViewModel.IsReplaceVariablesSettingsCardEnabled, Mode=OneWay}">
<StackPanel Orientation="Horizontal" Spacing="4"> <StackPanel Orientation="Horizontal" Spacing="4">
<ToggleSwitch x:Uid="ReplaceVariablesToggle" IsOn="{x:Bind ViewModel.ReplaceVariables, Mode=TwoWay}" /> <ToggleSwitch
x:Uid="ReplaceVariablesToggle"
AutomationProperties.Name="{Binding ElementName=NewPlusBehaviourReplaceVariablesToggle, Path=Header}"
IsOn="{x:Bind ViewModel.ReplaceVariables, Mode=TwoWay}" />
<Button <Button
x:Uid="FileCreationButton" x:Uid="FileCreationButton"
Width="28" Width="28"

View File

@@ -67,6 +67,7 @@
Header="{x:Bind Path=DisplayLabel}"> Header="{x:Bind Path=DisplayLabel}">
<ComboBox <ComboBox
MinWidth="{StaticResource SettingActionControlMinWidth}" MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.Name="{x:Bind Path=DisplayLabel}"
DisplayMemberPath="Key" DisplayMemberPath="Key"
ItemsSource="{x:Bind Path=ComboBoxItems}" ItemsSource="{x:Bind Path=ComboBoxItems}"
SelectedValue="{x:Bind ComboBoxValue, Mode=TwoWay}" SelectedValue="{x:Bind ComboBoxValue, Mode=TwoWay}"
@@ -110,6 +111,7 @@
Header="{x:Bind Path=DisplayLabel}"> Header="{x:Bind Path=DisplayLabel}">
<NumberBox <NumberBox
MinWidth="{StaticResource SettingActionControlMinWidth}" MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.Name="{x:Bind Path=DisplayLabel}"
LargeChange="{x:Bind NumberBoxLargeChange, Mode=OneWay}" LargeChange="{x:Bind NumberBoxLargeChange, Mode=OneWay}"
Maximum="{x:Bind NumberBoxMax, Mode=OneWay}" Maximum="{x:Bind NumberBoxMax, Mode=OneWay}"
Minimum="{x:Bind NumberBoxMin, Mode=OneWay}" Minimum="{x:Bind NumberBoxMin, Mode=OneWay}"
@@ -175,6 +177,7 @@
IsEnabled="{x:Bind SecondSettingIsEnabled, Mode=OneWay}"> IsEnabled="{x:Bind SecondSettingIsEnabled, Mode=OneWay}">
<ComboBox <ComboBox
MinWidth="{StaticResource SettingActionControlMinWidth}" MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.Name="{x:Bind Path=SecondDisplayLabel}"
DisplayMemberPath="Key" DisplayMemberPath="Key"
ItemsSource="{x:Bind Path=ComboBoxItems}" ItemsSource="{x:Bind Path=ComboBoxItems}"
SelectedValue="{x:Bind ComboBoxValue, Mode=TwoWay}" SelectedValue="{x:Bind ComboBoxValue, Mode=TwoWay}"
@@ -290,6 +293,7 @@
IsEnabled="{x:Bind SecondSettingIsEnabled, Mode=OneWay}"> IsEnabled="{x:Bind SecondSettingIsEnabled, Mode=OneWay}">
<NumberBox <NumberBox
MinWidth="{StaticResource SettingActionControlMinWidth}" MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.Name="{x:Bind Path=SecondDisplayLabel}"
LargeChange="{x:Bind NumberBoxLargeChange, Mode=OneWay}" LargeChange="{x:Bind NumberBoxLargeChange, Mode=OneWay}"
Maximum="{x:Bind NumberBoxMax, Mode=OneWay}" Maximum="{x:Bind NumberBoxMax, Mode=OneWay}"
Minimum="{x:Bind NumberBoxMin, Mode=OneWay}" Minimum="{x:Bind NumberBoxMin, Mode=OneWay}"

View File

@@ -22,7 +22,10 @@
Name="PowerRenameToggleEnable" Name="PowerRenameToggleEnable"
x:Uid="PowerRename_Toggle_Enable" x:Uid="PowerRename_Toggle_Enable"
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/PowerRename.png}"> HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/PowerRename.png}">
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" /> <ToggleSwitch
x:Uid="ToggleSwitch"
AutomationProperties.Name="{Binding ElementName=PowerRenameToggleEnable, Path=Header}"
IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" />
</tkcontrols:SettingsCard> </tkcontrols:SettingsCard>
</controls:GPOInfoControl> </controls:GPOInfoControl>
<controls:SettingsGroup x:Uid="PowerRename_ShellIntegration" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}"> <controls:SettingsGroup x:Uid="PowerRename_ShellIntegration" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
@@ -30,7 +33,10 @@
Name="PowerRenameToggleContextMenu" Name="PowerRenameToggleContextMenu"
x:Uid="PowerRename_Toggle_ContextMenu" x:Uid="PowerRename_Toggle_ContextMenu"
IsExpanded="False"> IsExpanded="False">
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.EnabledOnContextExtendedMenu, Mode=TwoWay, Converter={StaticResource BoolToComboBoxIndexConverter}}"> <ComboBox
MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.Name="{Binding ElementName=PowerRenameToggleContextMenu, Path=Header}"
SelectedIndex="{x:Bind ViewModel.EnabledOnContextExtendedMenu, Mode=TwoWay, Converter={StaticResource BoolToComboBoxIndexConverter}}">
<ComboBoxItem x:Uid="PowerRename_Toggle_StandardContextMenu" /> <ComboBoxItem x:Uid="PowerRename_Toggle_StandardContextMenu" />
<ComboBoxItem x:Uid="PowerRename_Toggle_ExtendedContextMenu" /> <ComboBoxItem x:Uid="PowerRename_Toggle_ExtendedContextMenu" />
</ComboBox> </ComboBox>
@@ -53,7 +59,10 @@
Name="PowerRenameToggleAutoComplete" Name="PowerRenameToggleAutoComplete"
x:Uid="PowerRename_Toggle_AutoComplete" x:Uid="PowerRename_Toggle_AutoComplete"
IsExpanded="True"> IsExpanded="True">
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.MRUEnabled, Mode=TwoWay}" /> <ToggleSwitch
x:Uid="ToggleSwitch"
AutomationProperties.Name="{Binding ElementName=PowerRenameToggleAutoComplete, Path=Header}"
IsOn="{x:Bind ViewModel.MRUEnabled, Mode=TwoWay}" />
<tkcontrols:SettingsExpander.Items> <tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard <tkcontrols:SettingsCard
Name="PowerRenameToggleMaxDispListNum" Name="PowerRenameToggleMaxDispListNum"
@@ -61,6 +70,7 @@
IsEnabled="{x:Bind ViewModel.GlobalAndMruEnabled, Mode=OneWay}"> IsEnabled="{x:Bind ViewModel.GlobalAndMruEnabled, Mode=OneWay}">
<NumberBox <NumberBox
MinWidth="{StaticResource SettingActionControlMinWidth}" MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.Name="{Binding ElementName=PowerRenameToggleMaxDispListNum, Path=Header}"
Maximum="20" Maximum="20"
Minimum="0" Minimum="0"
SpinButtonPlacementMode="Compact" SpinButtonPlacementMode="Compact"
@@ -73,12 +83,18 @@
Name="PowerRenameToggleRestoreFlagsOnLaunch" Name="PowerRenameToggleRestoreFlagsOnLaunch"
x:Uid="PowerRename_Toggle_RestoreFlagsOnLaunch" x:Uid="PowerRename_Toggle_RestoreFlagsOnLaunch"
HeaderIcon="{ui:FontIcon Glyph=&#xe81c;}"> HeaderIcon="{ui:FontIcon Glyph=&#xe81c;}">
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.RestoreFlagsOnLaunch, Mode=TwoWay}" /> <ToggleSwitch
x:Uid="ToggleSwitch"
AutomationProperties.Name="{Binding ElementName=PowerRenameToggleRestoreFlagsOnLaunch, Path=Header}"
IsOn="{x:Bind ViewModel.RestoreFlagsOnLaunch, Mode=TwoWay}" />
</tkcontrols:SettingsCard> </tkcontrols:SettingsCard>
</controls:SettingsGroup> </controls:SettingsGroup>
<controls:SettingsGroup x:Uid="PowerRename_BehaviorHeader" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}"> <controls:SettingsGroup x:Uid="PowerRename_BehaviorHeader" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsCard Name="PowerRenameToggleUseBoostLib" x:Uid="PowerRename_Toggle_UseBoostLib"> <tkcontrols:SettingsCard Name="PowerRenameToggleUseBoostLib" x:Uid="PowerRename_Toggle_UseBoostLib">
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.UseBoostLib, Mode=TwoWay}" /> <ToggleSwitch
x:Uid="ToggleSwitch"
AutomationProperties.Name="{Binding ElementName=PowerRenameToggleUseBoostLib, Path=Header}"
IsOn="{x:Bind ViewModel.UseBoostLib, Mode=TwoWay}" />
</tkcontrols:SettingsCard> </tkcontrols:SettingsCard>
</controls:SettingsGroup> </controls:SettingsGroup>
</StackPanel> </StackPanel>

View File

@@ -241,6 +241,12 @@
<ComboBoxItem>1.0</ComboBoxItem> <ComboBoxItem>1.0</ComboBoxItem>
</ComboBox> </ComboBox>
</tkcontrols:SettingsCard> </tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="ZoomItRecordFormat" x:Uid="ZoomIt_Record_Format">
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind Path=ViewModel.RecordFormatIndex, Mode=TwoWay}">
<ComboBoxItem>GIF</ComboBoxItem>
<ComboBoxItem>MP4</ComboBoxItem>
</ComboBox>
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="ZoomItRecordCaptureAudio" x:Uid="ZoomIt_Record_CaptureAudio"> <tkcontrols:SettingsCard Name="ZoomItRecordCaptureAudio" x:Uid="ZoomIt_Record_CaptureAudio">
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.RecordCaptureAudio, Mode=TwoWay}" /> <ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.RecordCaptureAudio, Mode=TwoWay}" />
</tkcontrols:SettingsCard> </tkcontrols:SettingsCard>

View File

@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
Version 2.0 Version 2.0
The primary goals of this format is to allow a simple XML format The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes various data types are done through the TypeConverter classes
associated with the data types. associated with the data types.
Example: Example:
... ado.net/XML headers & schema ... ... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader> <resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment> <comment>This is a comment</comment>
</data> </data>
There are any number of "resheader" rows that contain simple There are any number of "resheader" rows that contain simple
name/value pairs. name/value pairs.
Each data row contains a name, and value. The row also contains a Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture. text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the Classes that don't support this are serialized and stored with the
mimetype set. mimetype set.
The mimetype is used for serialized objects, and tells the The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly: extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below. read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64 mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64 mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64 mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter : using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
@@ -2251,7 +2251,7 @@ Take a moment to preview the various utilities listed or view our comprehensive
<value>Press the **Restart as administrator** button from the File Locksmith UI to also get information on elevated processes that might be using the files.</value> <value>Press the **Restart as administrator** button from the File Locksmith UI to also get information on elevated processes that might be using the files.</value>
</data> </data>
<data name="Oobe_FileExplorer_HowToEnable.Text" xml:space="preserve"> <data name="Oobe_FileExplorer_HowToEnable.Text" xml:space="preserve">
<value>Select **View** which is located at the top of File Explorer, followed by **Show**, and then **Preview pane**. <value>Select **View** which is located at the top of File Explorer, followed by **Show**, and then **Preview pane**.
From there, simply click on one of the supported files in the File Explorer and observe the content on the preview pane!</value> From there, simply click on one of the supported files in the File Explorer and observe the content on the preview pane!</value>
</data> </data>
<data name="Oobe_HowToCreateMappings.Text" xml:space="preserve"> <data name="Oobe_HowToCreateMappings.Text" xml:space="preserve">
@@ -5028,6 +5028,9 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
<data name="ZoomIt_Record_Scaling.Header" xml:space="preserve"> <data name="ZoomIt_Record_Scaling.Header" xml:space="preserve">
<value>Scaling</value> <value>Scaling</value>
</data> </data>
<data name="ZoomIt_Record_Format.Header" xml:space="preserve">
<value>Format</value>
</data>
<data name="ZoomIt_Record_CaptureAudio.Header" xml:space="preserve"> <data name="ZoomIt_Record_CaptureAudio.Header" xml:space="preserve">
<value>Capture audio input</value> <value>Capture audio input</value>
</data> </data>
@@ -5562,6 +5565,9 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
<data name="ShortcutConflictControl_Title.Text" xml:space="preserve"> <data name="ShortcutConflictControl_Title.Text" xml:space="preserve">
<value>Shortcut conflicts</value> <value>Shortcut conflicts</value>
</data> </data>
<data name="ShortcutConflictControl_Automation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Shortcut conflicts</value>
</data>
<data name="ShortcutConflictControl_NoConflictsFound" xml:space="preserve"> <data name="ShortcutConflictControl_NoConflictsFound" xml:space="preserve">
<value>No conflicts found</value> <value>No conflicts found</value>
</data> </data>

View File

@@ -652,6 +652,54 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
} }
} }
public int RecordFormatIndex
{
get
{
if (_zoomItSettings.Properties.RecordFormat.Value == "GIF")
{
return 0;
}
if (_zoomItSettings.Properties.RecordFormat.Value == "MP4")
{
return 1;
}
return 0;
}
set
{
int format = 0;
if (_zoomItSettings.Properties.RecordFormat.Value == "GIF")
{
format = 0;
}
if (_zoomItSettings.Properties.RecordFormat.Value == "MP4")
{
format = 1;
}
if (format != value)
{
_zoomItSettings.Properties.RecordFormat.Value = value == 0 ? "GIF" : "MP4";
OnPropertyChanged(nameof(RecordFormatIndex));
NotifySettingsChanged();
// Reload settings to get the new format's scaling value
var reloadedSettings = global::PowerToys.ZoomItSettingsInterop.ZoomItSettings.LoadSettingsJson();
var reloaded = JsonSerializer.Deserialize<ZoomItSettings>(reloadedSettings, _serializerOptions);
if (reloaded != null && reloaded.Properties != null)
{
_zoomItSettings.Properties.RecordScaling.Value = reloaded.Properties.RecordScaling.Value;
OnPropertyChanged(nameof(RecordScalingIndex));
}
}
}
}
public bool RecordCaptureAudio public bool RecordCaptureAudio
{ {
get => _zoomItSettings.Properties.CaptureAudio.Value; get => _zoomItSettings.Properties.CaptureAudio.Value;