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

@@ -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) }

View File

@@ -15,6 +15,7 @@
#include "Utility.h" #include "Utility.h"
#include "WindowsVersions.h" #include "WindowsVersions.h"
#include "ZoomItSettings.h" #include "ZoomItSettings.h"
#include "GifRecordingSession.h"
#ifdef __ZOOMIT_POWERTOYS__ #ifdef __ZOOMIT_POWERTOYS__
#include <common/interop/shared_constants.h> #include <common/interop/shared_constants.h>
@@ -68,6 +69,8 @@ COLORREF g_CustomColors[16];
#define SNIP_SAVE_HOTKEY 9 #define SNIP_SAVE_HOTKEY 9
#define DEMOTYPE_HOTKEY 10 #define DEMOTYPE_HOTKEY 10
#define DEMOTYPE_RESET_HOTKEY 11 #define DEMOTYPE_RESET_HOTKEY 11
#define RECORD_GIF_HOTKEY 12
#define RECORD_GIF_WINDOW_HOTKEY 13
#define ZOOM_PAGE 0 #define ZOOM_PAGE 0
#define LIVE_PAGE 1 #define LIVE_PAGE 1
@@ -89,6 +92,11 @@ OPTION_TABS g_OptionsTabs[] = {
{ _T("Snip"), NULL } { _T("Snip"), NULL }
}; };
static const TCHAR* g_RecordingFormats[] = {
_T("GIF"),
_T("MP4")
};
float g_ZoomLevels[] = { float g_ZoomLevels[] = {
1.25, 1.25,
1.50, 1.50,
@@ -99,6 +107,8 @@ float g_ZoomLevels[] = {
}; };
DWORD g_FramerateOptions[] = { DWORD g_FramerateOptions[] = {
15,
24,
30, 30,
60 60
}; };
@@ -152,12 +162,15 @@ BOOLEAN g_running = TRUE;
// Screen recording globals // Screen recording globals
#define DEFAULT_RECORDING_FILE L"Recording.mp4" #define DEFAULT_RECORDING_FILE L"Recording.mp4"
#define DEFAULT_GIF_RECORDING_FILE L"Recording.gif"
BOOL g_RecordToggle = FALSE; BOOL g_RecordToggle = FALSE;
BOOL g_RecordCropping = FALSE; BOOL g_RecordCropping = FALSE;
SelectRectangle g_SelectRectangle; SelectRectangle g_SelectRectangle;
std::wstring g_RecordingSaveLocation; std::wstring g_RecordingSaveLocation;
winrt::IDirect3DDevice g_RecordDevice{ nullptr }; winrt::IDirect3DDevice g_RecordDevice{ nullptr };
std::shared_ptr<VideoRecordingSession> g_RecordingSession = nullptr; std::shared_ptr<VideoRecordingSession> g_RecordingSession = nullptr;
std::shared_ptr<GifRecordingSession> g_GifRecordingSession = nullptr;
type_pGetMonitorInfo pGetMonitorInfo; type_pGetMonitorInfo pGetMonitorInfo;
type_MonitorFromPoint pMonitorFromPoint; type_MonitorFromPoint pMonitorFromPoint;
@@ -1771,6 +1784,58 @@ INT_PTR CALLBACK OptionsTabProc( HWND hDlg, UINT message,
case WM_INITDIALOG: case WM_INITDIALOG:
return TRUE; return TRUE;
case WM_COMMAND: case WM_COMMAND:
// Handle combo box selection changes
if (HIWORD(wParam) == CBN_SELCHANGE) {
if (LOWORD(wParam) == IDC_RECORD_SCALING) {
int format = static_cast<int>(SendMessage(GetDlgItem(hDlg, IDC_RECORD_FORMAT), CB_GETCURSEL, 0, 0));
int scale = static_cast<int>(SendMessage(GetDlgItem(hDlg, IDC_RECORD_SCALING), CB_GETCURSEL, 0, 0));
if(format == 0)
{
g_RecordScalingGIF = static_cast<BYTE>((scale + 1) * 10);
}
else
{
g_RecordScalingMP4 = static_cast<BYTE>((scale + 1) * 10);
}
}
else if (LOWORD(wParam) == IDC_RECORD_FORMAT) {
// Get the currently selected format
int selection = static_cast<int>(SendMessage(GetDlgItem(hDlg, IDC_RECORD_FORMAT),
CB_GETCURSEL, 0, 0));
// Get the selected text to check if it's GIF
TCHAR selectedText[32] = {0};
SendMessage(GetDlgItem(hDlg, IDC_RECORD_FORMAT),
CB_GETLBTEXT, selection, reinterpret_cast<LPARAM>(selectedText));
// Check if GIF is selected by comparing the text
bool isGifSelected = (wcscmp(selectedText, L"GIF") == 0);
// if gif is selected set the scaling to the g_recordScaleGIF value otherwise to the g_recordScaleMP4 value
if (isGifSelected) {
g_RecordScaling = g_RecordScalingGIF;
} else {
g_RecordScaling = g_RecordScalingMP4;
}
for (int i = 0; i < 10; i++) {
int scalingValue = (i + 1) * 10;
if (scalingValue == static_cast<int>(g_RecordScaling)) {
SendMessage(GetDlgItem(hDlg, IDC_RECORD_SCALING),
CB_SETCURSEL, i, 0);
break;
}
}
// Enable/disable microphone controls based on selection
EnableWindow(GetDlgItem(hDlg, IDC_MICROPHONE), !isGifSelected);
EnableWindow(GetDlgItem(hDlg, IDC_CAPTURE_AUDIO), !isGifSelected);
}
}
switch ( LOWORD( wParam )) { switch ( LOWORD( wParam )) {
case IDC_ADVANCED_BREAK: case IDC_ADVANCED_BREAK:
DialogBox( g_hInstance, L"ADVANCED_BREAK", hDlg, AdvancedBreakProc ); DialogBox( g_hInstance, L"ADVANCED_BREAK", hDlg, AdvancedBreakProc );
@@ -1914,6 +1979,8 @@ void UnregisterAllHotkeys( HWND hWnd )
UnregisterHotKey( hWnd, SNIP_SAVE_HOTKEY); UnregisterHotKey( hWnd, SNIP_SAVE_HOTKEY);
UnregisterHotKey( hWnd, DEMOTYPE_HOTKEY ); UnregisterHotKey( hWnd, DEMOTYPE_HOTKEY );
UnregisterHotKey( hWnd, DEMOTYPE_RESET_HOTKEY ); UnregisterHotKey( hWnd, DEMOTYPE_RESET_HOTKEY );
UnregisterHotKey( hWnd, RECORD_GIF_HOTKEY );
UnregisterHotKey( hWnd, RECORD_GIF_WINDOW_HOTKEY );
} }
//---------------------------------------------------------------------------- //----------------------------------------------------------------------------
@@ -1943,6 +2010,9 @@ void RegisterAllHotkeys(HWND hWnd)
RegisterHotKey(hWnd, RECORD_CROP_HOTKEY, (g_RecordToggleMod ^ MOD_SHIFT) | MOD_NOREPEAT, g_RecordToggleKey & 0xFF); RegisterHotKey(hWnd, RECORD_CROP_HOTKEY, (g_RecordToggleMod ^ MOD_SHIFT) | MOD_NOREPEAT, g_RecordToggleKey & 0xFF);
RegisterHotKey(hWnd, RECORD_WINDOW_HOTKEY, (g_RecordToggleMod ^ MOD_ALT) | MOD_NOREPEAT, g_RecordToggleKey & 0xFF); RegisterHotKey(hWnd, RECORD_WINDOW_HOTKEY, (g_RecordToggleMod ^ MOD_ALT) | MOD_NOREPEAT, g_RecordToggleKey & 0xFF);
} }
// Register CTRL+8 for GIF recording and CTRL+ALT+8 for GIF window recording
RegisterHotKey(hWnd, RECORD_GIF_HOTKEY, MOD_CONTROL | MOD_NOREPEAT, 568 && 0xFF);
RegisterHotKey(hWnd, RECORD_GIF_WINDOW_HOTKEY, MOD_CONTROL | MOD_ALT | MOD_NOREPEAT, 568 && 0xFF);
} }
@@ -2113,12 +2183,33 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FRAME_RATE), CB_SETCURSEL, static_cast<WPARAM>(i), static_cast<LPARAM>(0)); SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FRAME_RATE), CB_SETCURSEL, static_cast<WPARAM>(i), static_cast<LPARAM>(0));
} }
} }
// Add the recording format to the combo box and set the current selection
size_t selection = 0;
const wchar_t* currentFormatString = (g_RecordingFormat == RecordingFormat::GIF) ? L"GIF" : L"MP4";
for( size_t i = 0; i < (sizeof(g_RecordingFormats) / sizeof(g_RecordingFormats[0])); i++ )
{
SendMessage( GetDlgItem( g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FORMAT ), static_cast<UINT>(CB_ADDSTRING), static_cast<WPARAM>(0), reinterpret_cast<LPARAM>(g_RecordingFormats[i]) );
if( selection == 0 && wcscmp( g_RecordingFormats[i], currentFormatString ) == 0 )
{
selection = i;
}
}
SendMessage( GetDlgItem( g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FORMAT ), CB_SETCURSEL, static_cast<WPARAM>(selection), static_cast<LPARAM>(0) );
for(unsigned int i = 1; i < 11; i++) { for(unsigned int i = 1; i < 11; i++) {
_stprintf(text, L"%2.1f", (static_cast<double>(i)) / 10 ); _stprintf(text, L"%2.1f", (static_cast<double>(i)) / 10 );
SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_SCALING), static_cast<UINT>(CB_ADDSTRING), SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_SCALING), static_cast<UINT>(CB_ADDSTRING),
static_cast<WPARAM>(0), reinterpret_cast<LPARAM>(text)); static_cast<WPARAM>(0), reinterpret_cast<LPARAM>(text));
if (g_RecordScaling == i*10 ) {
if (g_RecordingFormat == RecordingFormat::GIF && i*10 == g_RecordScalingGIF ) {
SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_SCALING), CB_SETCURSEL, static_cast<WPARAM>(i)-1, static_cast<LPARAM>(0));
}
if (g_RecordingFormat == RecordingFormat::MP4 && i*10 == g_RecordScalingMP4 ) {
SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_SCALING), CB_SETCURSEL, static_cast<WPARAM>(i)-1, static_cast<LPARAM>(0)); SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_SCALING), CB_SETCURSEL, static_cast<WPARAM>(i)-1, static_cast<LPARAM>(0));
} }
@@ -2136,7 +2227,7 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
// Add the microphone devices to the combo box and set the current selection // Add the microphone devices to the combo box and set the current selection
SendMessage( GetDlgItem( g_OptionsTabs[RECORD_PAGE].hPage, IDC_MICROPHONE ), static_cast<UINT>(CB_ADDSTRING), static_cast<WPARAM>(0), reinterpret_cast<LPARAM>(L"Default")); SendMessage( GetDlgItem( g_OptionsTabs[RECORD_PAGE].hPage, IDC_MICROPHONE ), static_cast<UINT>(CB_ADDSTRING), static_cast<WPARAM>(0), reinterpret_cast<LPARAM>(L"Default"));
size_t selection = 0; selection = 0;
for( size_t i = 0; i < microphones.size(); i++ ) for( size_t i = 0; i < microphones.size(); i++ )
{ {
SendMessage( GetDlgItem( g_OptionsTabs[RECORD_PAGE].hPage, IDC_MICROPHONE ), static_cast<UINT>(CB_ADDSTRING), static_cast<WPARAM>(0), reinterpret_cast<LPARAM>(microphones[i].second.c_str()) ); SendMessage( GetDlgItem( g_OptionsTabs[RECORD_PAGE].hPage, IDC_MICROPHONE ), static_cast<UINT>(CB_ADDSTRING), static_cast<WPARAM>(0), reinterpret_cast<LPARAM>(microphones[i].second.c_str()) );
@@ -2147,6 +2238,11 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
} }
SendMessage( GetDlgItem( g_OptionsTabs[RECORD_PAGE].hPage, IDC_MICROPHONE ), CB_SETCURSEL, static_cast<WPARAM>(selection), static_cast<LPARAM>(0) ); SendMessage( GetDlgItem( g_OptionsTabs[RECORD_PAGE].hPage, IDC_MICROPHONE ), CB_SETCURSEL, static_cast<WPARAM>(selection), static_cast<LPARAM>(0) );
// Set initial state of microphone controls based on recording format
bool isGifSelected = (g_RecordingFormat == RecordingFormat::GIF);
EnableWindow(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_MICROPHONE), !isGifSelected);
EnableWindow(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_CAPTURE_AUDIO), !isGifSelected);
if( GetFileAttributes( g_DemoTypeFile ) == -1 ) if( GetFileAttributes( g_DemoTypeFile ) == -1 )
{ {
memset( g_DemoTypeFile, 0, sizeof( g_DemoTypeFile ) ); memset( g_DemoTypeFile, 0, sizeof( g_DemoTypeFile ) );
@@ -2249,7 +2345,17 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
text[2] = 0; text[2] = 0;
newTimeout = _tstoi( text ); newTimeout = _tstoi( text );
g_RecordFrameRate = g_FramerateOptions[SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FRAME_RATE), static_cast<UINT>(CB_GETCURSEL), static_cast<WPARAM>(0), static_cast<LPARAM>(0))]; if( g_RecordingFormat == RecordingFormat::GIF )
{
// Hardcode lower frame rate for GIFs
g_RecordFrameRate = 15;
}
else
{
g_RecordFrameRate = g_FramerateOptions[SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FRAME_RATE), static_cast<UINT>(CB_GETCURSEL), static_cast<WPARAM>(0), static_cast<LPARAM>(0))];
}
g_RecordingFormat = static_cast<RecordingFormat>(SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FORMAT), static_cast<UINT>(CB_GETCURSEL), static_cast<WPARAM>(0), static_cast<LPARAM>(0)));
g_RecordScaling = static_cast<int>(SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_SCALING), static_cast<UINT>(CB_GETCURSEL), static_cast<WPARAM>(0), static_cast<LPARAM>(0)) * 10 + 10); g_RecordScaling = static_cast<int>(SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_SCALING), static_cast<UINT>(CB_GETCURSEL), static_cast<WPARAM>(0), static_cast<LPARAM>(0)) * 10 + 10);
// Get the selected microphone // Get the selected microphone
@@ -3395,6 +3501,12 @@ void StopRecording()
g_RecordingSession = nullptr; g_RecordingSession = nullptr;
} }
if ( g_GifRecordingSession != nullptr ) {
g_GifRecordingSession->Close();
g_GifRecordingSession = nullptr;
}
g_RecordToggle = FALSE; g_RecordToggle = FALSE;
#if WINDOWS_CURSOR_RECORDING_WORKAROUND #if WINDOWS_CURSOR_RECORDING_WORKAROUND
@@ -3451,7 +3563,10 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
auto tempFolderPath = std::filesystem::temp_directory_path().wstring(); auto tempFolderPath = std::filesystem::temp_directory_path().wstring();
auto tempFolder = co_await winrt::StorageFolder::GetFolderFromPathAsync( tempFolderPath ); auto tempFolder = co_await winrt::StorageFolder::GetFolderFromPathAsync( tempFolderPath );
auto appFolder = co_await tempFolder.CreateFolderAsync( L"ZoomIt", winrt::CreationCollisionOption::OpenIfExists ); auto appFolder = co_await tempFolder.CreateFolderAsync( L"ZoomIt", winrt::CreationCollisionOption::OpenIfExists );
auto file = co_await appFolder.CreateFileAsync( L"zoomit.mp4", winrt::CreationCollisionOption::ReplaceExisting );
// Choose temp file extension based on format
const wchar_t* tempFileName = (g_RecordingFormat == RecordingFormat::GIF) ? L"zoomit.gif" : L"zoomit.mp4";
auto file = co_await appFolder.CreateFileAsync( tempFileName, winrt::CreationCollisionOption::ReplaceExisting );
// Get the device // Get the device
auto d3dDevice = util::CreateD3D11Device(); auto d3dDevice = util::CreateD3D11Device();
@@ -3474,21 +3589,40 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
item = util::CreateCaptureItemForMonitor( hMon ); item = util::CreateCaptureItemForMonitor( hMon );
auto stream = co_await file.OpenAsync( winrt::FileAccessMode::ReadWrite ); auto stream = co_await file.OpenAsync( winrt::FileAccessMode::ReadWrite );
g_RecordingSession = VideoRecordingSession::Create(
g_RecordDevice,
item,
*rcCrop,
g_RecordFrameRate,
g_CaptureAudio,
stream );
if( g_hWndLiveZoom != NULL ) // Create the appropriate recording session based on format
g_RecordingSession->EnableCursorCapture( false ); if (g_RecordingFormat == RecordingFormat::GIF)
{
g_GifRecordingSession = GifRecordingSession::Create(
g_RecordDevice,
item,
*rcCrop,
g_RecordFrameRate,
stream );
co_await g_RecordingSession->StartAsync(); if( g_hWndLiveZoom != NULL )
g_GifRecordingSession->EnableCursorCapture( false );
// g_RecordingSession isn't null if we're aborting a recording co_await g_GifRecordingSession->StartAsync();
if( g_RecordingSession == nullptr ) { }
else
{
g_RecordingSession = VideoRecordingSession::Create(
g_RecordDevice,
item,
*rcCrop,
g_RecordFrameRate,
g_CaptureAudio,
stream );
if( g_hWndLiveZoom != NULL )
g_RecordingSession->EnableCursorCapture( false );
co_await g_RecordingSession->StartAsync();
}
// Check if recording was aborted
if( g_RecordingSession == nullptr && g_GifRecordingSession == nullptr ) {
g_bSaveInProgress = true; g_bSaveInProgress = true;
@@ -3504,11 +3638,24 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
wil::com_ptr<IShellItem> videosItem; wil::com_ptr<IShellItem> videosItem;
if( SUCCEEDED ( SHGetKnownFolderItem( FOLDERID_Videos, KF_FLAG_DEFAULT, nullptr, IID_IShellItem, (void**) videosItem.put() ) ) ) if( SUCCEEDED ( SHGetKnownFolderItem( FOLDERID_Videos, KF_FLAG_DEFAULT, nullptr, IID_IShellItem, (void**) videosItem.put() ) ) )
saveDialog->SetDefaultFolder( videosItem.get() ); saveDialog->SetDefaultFolder( videosItem.get() );
saveDialog->SetDefaultExtension( L".mp4" );
COMDLG_FILTERSPEC fileTypes[] = { // Set file type based on the recording format
{ L"MP4 Video", L"*.mp4" } if (g_RecordingFormat == RecordingFormat::GIF)
}; {
saveDialog->SetFileTypes( _countof( fileTypes ), fileTypes ); saveDialog->SetDefaultExtension( L".gif" );
COMDLG_FILTERSPEC fileTypes[] = {
{ L"GIF Animation", L"*.gif" }
};
saveDialog->SetFileTypes( _countof( fileTypes ), fileTypes );
}
else
{
saveDialog->SetDefaultExtension( L".mp4" );
COMDLG_FILTERSPEC fileTypes[] = {
{ L"MP4 Video", L"*.mp4" }
};
saveDialog->SetFileTypes( _countof( fileTypes ), fileTypes );
}
if( g_RecordingSaveLocation.size() == 0) { if( g_RecordingSaveLocation.size() == 0) {
@@ -3516,8 +3663,12 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
wil::unique_cotaskmem_string folderPath; wil::unique_cotaskmem_string folderPath;
if (SUCCEEDED(saveDialog->GetFolder(shellItem.put())) && SUCCEEDED(shellItem->GetDisplayName(SIGDN_FILESYSPATH, folderPath.put()))) if (SUCCEEDED(saveDialog->GetFolder(shellItem.put())) && SUCCEEDED(shellItem->GetDisplayName(SIGDN_FILESYSPATH, folderPath.put())))
g_RecordingSaveLocation = folderPath.get(); g_RecordingSaveLocation = folderPath.get();
g_RecordingSaveLocation = std::filesystem::path{ g_RecordingSaveLocation } /= DEFAULT_RECORDING_FILE;
} }
// Always use appropriate default filename based on current format
std::filesystem::path currentPath{ g_RecordingSaveLocation };
const wchar_t* defaultFile = (g_RecordingFormat == RecordingFormat::GIF) ? DEFAULT_GIF_RECORDING_FILE : DEFAULT_RECORDING_FILE;
g_RecordingSaveLocation = currentPath.parent_path() / defaultFile;
auto suggestedName = GetUniqueRecordingFilename(); auto suggestedName = GetUniqueRecordingFilename();
saveDialog->SetFileName( suggestedName.c_str() ); saveDialog->SetFileName( suggestedName.c_str() );
@@ -3566,6 +3717,7 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
} }
co_await file.DeleteAsync(); co_await file.DeleteAsync();
g_RecordingSession = nullptr; g_RecordingSession = nullptr;
g_GifRecordingSession = nullptr;
} }
} catch( const winrt::hresult_error& error ) { } catch( const winrt::hresult_error& error ) {
@@ -3792,6 +3944,13 @@ LRESULT APIENTRY MainWndProc(
reg.ReadRegSettings( RegSettings ); reg.ReadRegSettings( RegSettings );
// Set g_RecordScaling based on the current recording format
if (g_RecordingFormat == RecordingFormat::GIF) {
g_RecordScaling = g_RecordScalingGIF;
} else {
g_RecordScaling = g_RecordScalingMP4;
}
// to support migrating from // to support migrating from
if ((g_PenColor >> 24) == 0) { if ((g_PenColor >> 24) == 0) {
g_PenColor |= 0xFF << 24; g_PenColor |= 0xFF << 24;
@@ -4206,6 +4365,8 @@ LRESULT APIENTRY MainWndProc(
case RECORD_HOTKEY: case RECORD_HOTKEY:
case RECORD_CROP_HOTKEY: case RECORD_CROP_HOTKEY:
case RECORD_WINDOW_HOTKEY: case RECORD_WINDOW_HOTKEY:
case RECORD_GIF_HOTKEY:
case RECORD_GIF_WINDOW_HOTKEY:
// //
// Recording // Recording
@@ -4335,7 +4496,7 @@ LRESULT APIENTRY MainWndProc(
cropRc = {}; cropRc = {};
// if we're recording a window, get the window // if we're recording a window, get the window
if (wParam == RECORD_WINDOW_HOTKEY) if (wParam == RECORD_WINDOW_HOTKEY || wParam == RECORD_GIF_WINDOW_HOTKEY)
{ {
GetCursorPos(&cursorPos); GetCursorPos(&cursorPos);
hWndRecord = WindowFromPoint(cursorPos); hWndRecord = WindowFromPoint(cursorPos);
@@ -4353,6 +4514,7 @@ LRESULT APIENTRY MainWndProc(
if( g_RecordToggle == FALSE ) if( g_RecordToggle == FALSE )
{ {
g_RecordToggle = TRUE; g_RecordToggle = TRUE;
#ifdef __ZOOMIT_POWERTOYS__ #ifdef __ZOOMIT_POWERTOYS__
if( g_StartedByPowerToys ) if( g_StartedByPowerToys )
{ {
@@ -6147,6 +6309,13 @@ LRESULT APIENTRY MainWndProc(
showOptions = TRUE; showOptions = TRUE;
} }
} }
// Register CTRL+8 for GIF recording and CTRL+ALT+8 for GIF window recording
if (!RegisterHotKey(hWnd, RECORD_GIF_HOTKEY, MOD_CONTROL | MOD_NOREPEAT, '8') ||
!RegisterHotKey(hWnd, RECORD_GIF_WINDOW_HOTKEY, MOD_CONTROL | MOD_ALT | MOD_NOREPEAT, '8'))
{
MessageBox(hWnd, L"The specified GIF recording hotkey is already in use.\nSelect a different GIF recording hotkey.", APPNAME, MB_ICONERROR);
showOptions = TRUE;
}
if (showOptions) if (showOptions)
{ {
// To open the PowerToys settings in the ZoomIt page. // To open the PowerToys settings in the ZoomIt page.

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

@@ -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

@@ -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;