diff --git a/.github/actions/spell-check/excludes.txt b/.github/actions/spell-check/excludes.txt index c6f1225788..551c248923 100644 --- a/.github/actions/spell-check/excludes.txt +++ b/.github/actions/spell-check/excludes.txt @@ -105,6 +105,7 @@ ^src/common/notifications/BackgroundActivatorDLL/cpp\.hint$ ^src/common/sysinternals/Eula/ ^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/launcher/Plugins/Microsoft\.PowerToys\.Run\.Plugin\.TimeDate/Properties/ ^src/modules/MouseUtils/MouseJumpUI/MainForm\.resx$ diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 4ed72eaa7a..d113aa3519 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -65,6 +65,7 @@ APIIs Apm APPBARDATA APPEXECLINK +appext APPLICATIONFRAMEHOST appmanifest APPMODEL @@ -100,7 +101,6 @@ ATX ATRIOX aumid authenticode -Authenticode AUTOBUDDY AUTOCHECKBOX AUTOHIDE @@ -187,6 +187,7 @@ CAPTUREBLT CAPTURECHANGED CARETBLINKING CAtl +CBN cch CCHDEVICENAME CCHFORMNAME @@ -315,7 +316,6 @@ CURSORINFO cursorpos CURSORSHOWING CURSORWRAP -CursorWrap customaction CUSTOMACTIONTEST CUSTOMFORMATPLACEHOLDER @@ -415,6 +415,9 @@ DNLEN DONOTROUND DONTVALIDATEPATH dotnet +downsampled +downsampling +Downsampled downscale DPICHANGED DPIs @@ -431,7 +434,6 @@ DSTINVERT DString DSVG dto -DTo DUMMYUNIONNAME dutil DVASPECT @@ -465,7 +467,6 @@ EDITKEYBOARD EDITSHORTCUTS EDITTEXT EFile -ekus eku emojis ENABLEDELAYEDEXPANSION @@ -601,6 +602,7 @@ getfilesiginforedist geolocator GETHOTKEY GETICON +GETLBTEXT GETMINMAXINFO GETNONCLIENTMETRICS GETPROPERTYSTOREFLAGS @@ -608,6 +610,7 @@ GETSCREENSAVERRUNNING GETSECKEY GETSTICKYKEYS GETTEXTLENGTH +GIFs gitmodules GHND GMEM @@ -618,6 +621,7 @@ GPOCA gpp gpu gradians +grctlext Gridcustomlayout GSM gtm @@ -1150,7 +1154,6 @@ NONCLIENTMETRICSW NONELEVATED nonspace nonstd -nullrefs NOOWNERZORDER NOPARENTNOTIFY NOPREFIX @@ -1190,8 +1193,8 @@ ntfs NTSTATUS NTSYSAPI NULLCURSOR -nullref nullonfailure +nullref numberbox nwc ocr diff --git a/.github/actions/spell-check/patterns.txt b/.github/actions/spell-check/patterns.txt index cb303a10ad..181d728e84 100644 --- a/.github/actions/spell-check/patterns.txt +++ b/.github/actions/spell-check/patterns.txt @@ -253,7 +253,7 @@ _SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING # hit-count: 1 file-count: 1 # Amazon -\bamazon\.com/[-\w]+/(?:dp/[0-9A-Z]+|)[^"'\s]+ +\bamazon\.com/[-\w]+/(?:dp/[0-9A-Z]+|) # hit-count: 3 file-count: 3 # imgur diff --git a/.pipelines/v2/release.yml b/.pipelines/v2/release.yml index e8cc7d5ed8..71f80f574b 100644 --- a/.pipelines/v2/release.yml +++ b/.pipelines/v2/release.yml @@ -52,8 +52,6 @@ extends: name: SHINE-INT-S ${{ if eq(parameters.useVSPreview, true) }}: demands: ImageOverride -equals SHINE-VS17-Preview - ${{ else }}: - image: SHINE-VS17-Latest os: windows sdl: tsa: @@ -75,7 +73,6 @@ extends: name: SHINE-INT-L demands: # Our INT agents have a large disk mounted at P:\ - - WorkFolder -equals P:\_work - ${{ if eq(parameters.useVSPreview, true) }}: - ImageOverride -equals SHINE-VS17-Preview os: windows @@ -126,7 +123,6 @@ extends: parameters: pool: name: SHINE-INT-L - image: SHINE-VS17-Latest os: windows official: true codeSign: true diff --git a/.pipelines/v2/templates/job-build-project.yml b/.pipelines/v2/templates/job-build-project.yml index 34e4ce4340..62cb993d5d 100644 --- a/.pipelines/v2/templates/job-build-project.yml +++ b/.pipelines/v2/templates/job-build-project.yml @@ -111,6 +111,7 @@ jobs: ${{ else }}: OutputBuildPlatform: ${{ platform }} 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' # Azure DevOps abhors a vacuum # 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 artifactName: $(JobOutputArtifactName) targetPath: $(Build.ArtifactStagingDirectory) + - output: pipelineArtifact + artifactName: $(JobOutputArtifactName)-failure-$(System.JobAttempt) + targetPath: $(LogOutputDirectory) + condition: or(failed(), canceled()) steps: - checkout: self 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. - task: CopyFiles@2 displayName: HACK Copy core WebView2 ARM64 dll to output directory - condition: eq(variables['BuildPlatform'],'arm64') + condition: and(succeeded(), eq(variables['BuildPlatform'], 'arm64')) inputs: 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/ @@ -434,11 +439,11 @@ jobs: inputs: testResultsFormat: VSTest testResultsFiles: '**/*.trx' - condition: ne(variables['BuildPlatform'],'arm64') + condition: and(succeeded(), ne(variables['BuildPlatform'], 'arm64')) # Native dlls - 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' inputs: platform: '$(BuildPlatform)' diff --git a/README.md b/README.md index e737281523..067d6d6f50 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,11 @@

Installation - . + · Documentation - . + · Blog - . + · Release notes



diff --git a/doc/images/icons/CursorWrap.png b/doc/images/icons/CursorWrap.png new file mode 100644 index 0000000000..20db84fc9a Binary files /dev/null and b/doc/images/icons/CursorWrap.png differ diff --git a/doc/images/icons/Find My Mouse.png b/doc/images/icons/Find My Mouse.png index 71dd994569..82fbe59800 100644 Binary files a/doc/images/icons/Find My Mouse.png and b/doc/images/icons/Find My Mouse.png differ diff --git a/doc/images/icons/Mouse Highlighter.png b/doc/images/icons/Mouse Highlighter.png index b06843d941..0feb5cc15a 100644 Binary files a/doc/images/icons/Mouse Highlighter.png and b/doc/images/icons/Mouse Highlighter.png differ diff --git a/doc/images/icons/MouseJump.png b/doc/images/icons/MouseJump.png new file mode 100644 index 0000000000..2fbe450ac2 Binary files /dev/null and b/doc/images/icons/MouseJump.png differ diff --git a/doc/images/icons/MouseWithoutBorders.png b/doc/images/icons/MouseWithoutBorders.png index a29adf7d11..ee66893cbd 100644 Binary files a/doc/images/icons/MouseWithoutBorders.png and b/doc/images/icons/MouseWithoutBorders.png differ diff --git a/src/common/UITestAutomation/SettingsConfigHelper.cs b/src/common/UITestAutomation/SettingsConfigHelper.cs new file mode 100644 index 0000000000..833ec4f19d --- /dev/null +++ b/src/common/UITestAutomation/SettingsConfigHelper.cs @@ -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 +{ + /// + /// Helper class for configuring PowerToys settings for UI tests. + /// + public class SettingsConfigHelper + { + private static readonly JsonSerializerOptions IndentedJsonOptions = new() { WriteIndented = true }; + private static readonly SettingsUtils SettingsUtils = new SettingsUtils(); + + /// + /// Configures global PowerToys settings to enable only specified modules and disable all others. + /// + /// Array of module names to enable (e.g., "Peek", "FancyZones"). All other modules will be disabled. + /// Thrown when modulesToEnable is null. + /// Thrown when settings file operations fail. + [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(); + } + 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(); + + 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>(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); + } + } + + /// + /// 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. + /// + /// The name of the module (e.g., "Peek", "FancyZones"). + /// The default JSON content to use if the settings file doesn't exist. + /// + /// 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<string, object> and not return a value. + /// Example: (settings) => { ((Dictionary<string, object>)settings["properties"])["SomeSetting"] = newValue; } + /// + /// Thrown when moduleName or updateSettingsAction is null. + /// Thrown when settings file operations fail. + [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> 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? 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>(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>(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); + } + } + } +} diff --git a/src/common/UITestAutomation/UITestAutomation.csproj b/src/common/UITestAutomation/UITestAutomation.csproj index add7acfeb9..549b8a430b 100644 --- a/src/common/UITestAutomation/UITestAutomation.csproj +++ b/src/common/UITestAutomation/UITestAutomation.csproj @@ -8,7 +8,7 @@ enable true true - net9.0-windows10.0.22621.0 + net9.0-windows10.0.26100.0 true false @@ -21,4 +21,8 @@ + + + + diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml index 9a6b8d04c9..8b7e3e5c6a 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml @@ -542,7 +542,10 @@ Source="{x:Bind ViewModel.ActiveAIProvider?.ServiceType, Mode=OneWay, Converter={StaticResource ServiceTypeToIconConverter}}" /> - + + NavigateUri="{x:Bind ViewModel.TermsLinkUri, Mode=OneWay}" + Visibility="{x:Bind ViewModel.HasTermsLink, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" /> + NavigateUri="{x:Bind ViewModel.PrivacyLinkUri, Mode=OneWay}" + Visibility="{x:Bind ViewModel.HasPrivacyLink, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" /> diff --git a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs index f183efee53..467497b9a9 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs @@ -71,6 +71,11 @@ namespace AdvancedPaste.ViewModels [NotifyPropertyChangedFor(nameof(IsCustomAIAvailable))] [NotifyPropertyChangedFor(nameof(AllowedAIProviders))] [NotifyPropertyChangedFor(nameof(ActiveAIProvider))] + [NotifyPropertyChangedFor(nameof(ActiveAIProviderTooltip))] + [NotifyPropertyChangedFor(nameof(TermsLinkUri))] + [NotifyPropertyChangedFor(nameof(PrivacyLinkUri))] + [NotifyPropertyChangedFor(nameof(HasTermsLink))] + [NotifyPropertyChangedFor(nameof(HasPrivacyLink))] private bool _isAllowedByGPO; [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 ClipboardHasDataForCustomAI => PasteFormat.SupportsClipboardFormats(CustomAIFormat, AvailableClipboardFormats); @@ -276,8 +310,8 @@ namespace AdvancedPaste.ViewModels OnPropertyChanged(nameof(IsAdvancedAIEnabled)); OnPropertyChanged(nameof(AIProviders)); OnPropertyChanged(nameof(AllowedAIProviders)); - OnPropertyChanged(nameof(ActiveAIProvider)); - OnPropertyChanged(nameof(ActiveAIProviderTooltip)); + + NotifyActiveProviderChanged(); EnqueueRefreshPasteFormats(); } @@ -316,8 +350,17 @@ namespace AdvancedPaste.ViewModels } } + NotifyActiveProviderChanged(); + } + + private void NotifyActiveProviderChanged() + { OnPropertyChanged(nameof(ActiveAIProvider)); OnPropertyChanged(nameof(ActiveAIProviderTooltip)); + OnPropertyChanged(nameof(TermsLinkUri)); + OnPropertyChanged(nameof(PrivacyLinkUri)); + OnPropertyChanged(nameof(HasTermsLink)); + OnPropertyChanged(nameof(HasPrivacyLink)); } private void RefreshPasteFormats() @@ -836,6 +879,7 @@ namespace AdvancedPaste.ViewModels UpdateAIProviderActiveFlags(); OnPropertyChanged(nameof(AIProviders)); + NotifyActiveProviderChanged(); EnqueueRefreshPasteFormats(); } diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj index 74d87c5623..fbe27a7af7 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj +++ b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj @@ -46,7 +46,7 @@ _DEBUG;NEWPLUSSHELLEXTENSIONWIN10_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) true Use - ..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories);..\NewShellExtensionContextMenu + ..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories);..\NewShellExtensionContextMenu;$(MSBuildThisFileDirectory)Generated Files false stdcpplatest @@ -67,7 +67,7 @@ NDEBUG;NEWPLUSSHELLEXTENSIONWIN10_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) true Use - ..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories);..\NewShellExtensionContextMenu + ..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories);..\NewShellExtensionContextMenu;$(MSBuildThisFileDirectory)Generated Files false stdcpplatest diff --git a/src/modules/ZoomIt/ZoomIt/GifRecordingSession.cpp b/src/modules/ZoomIt/ZoomIt/GifRecordingSession.cpp new file mode 100644 index 0000000000..22e2079f71 --- /dev/null +++ b/src/modules/ZoomIt/ZoomIt/GifRecordingSession.cpp @@ -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 + +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(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(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 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 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(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(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::Create( + winrt::IDirect3DDevice const& device, + winrt::GraphicsCaptureItem const& item, + RECT const& crop, + uint32_t frameRate, + winrt::Streams::IRandomAccessStream const& stream) +{ + return std::shared_ptr(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(m_width) || frameDesc.Height > static_cast(m_height)) + { + float scaleX = static_cast(m_width) / frameDesc.Width; + float scaleY = static_cast(m_height) / frameDesc.Height; + float scale = min(scaleX, scaleY); + + targetWidth = static_cast(frameDesc.Width * scale); + targetHeight = static_cast(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 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 frameEncode; + winrt::com_ptr 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 sourceBitmap; + winrt::check_hresult(m_wicFactory->CreateBitmapFromMemory( + frameDesc.Width, + frameDesc.Height, + GUID_WICPixelFormat32bppBGRA, + mappedResource.RowPitch, + frameDesc.Height * mappedResource.RowPitch, + static_cast(mappedResource.pData), + sourceBitmap.put())); + + // If we need downsampling, use WIC scaler + winrt::com_ptr finalSource = sourceBitmap; + if (targetWidth != frameDesc.Width || targetHeight != frameDesc.Height) + { + winrt::com_ptr 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 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(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 lastCroppedTexture; + + while (m_isRecording && !m_closed) + { + captureAttempts++; + auto frame = m_frameWait->TryGetNextFrame(); + + winrt::com_ptr croppedTexture; + + if (frame) + { + successfulCaptures++; + auto contentSize = frame->ContentSize; + auto frameTexture = GetDXGIInterfaceFromObject(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(0), static_cast(desc.Width)); + region.right = std::clamp(m_rcCrop.left + width, static_cast(0), static_cast(desc.Width)); + region.top = std::clamp(m_rcCrop.top, static_cast(0), static_cast(desc.Height)); + region.bottom = std::clamp(m_rcCrop.top + height, static_cast(0), static_cast(desc.Height)); + region.back = 1; + + // Copy the cropped region + m_d3dContext->CopySubresourceRegion( + croppedTexture.get(), + 0, + 0, 0, 0, + frameTexture.get(), + 0, + ®ion); + + // 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(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(); +} diff --git a/src/modules/ZoomIt/ZoomIt/GifRecordingSession.h b/src/modules/ZoomIt/ZoomIt/GifRecordingSession.h new file mode 100644 index 0000000000..90732f60f3 --- /dev/null +++ b/src/modules/ZoomIt/ZoomIt/GifRecordingSession.h @@ -0,0 +1,69 @@ +//============================================================================== +// +// Zoomit +// Sysinternals - www.sysinternals.com +// +// GIF recording support using Windows Imaging Component (WIC) +// +//============================================================================== +#pragma once + +#include "CaptureFrameWait.h" +#include +#include + +class GifRecordingSession : public std::enable_shared_from_this +{ +public: + [[nodiscard]] static std::shared_ptr 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 m_d3dDevice; + winrt::com_ptr m_d3dContext; + RECT m_rcCrop; + uint32_t m_frameRate; + + winrt::GraphicsCaptureItem m_item{ nullptr }; + winrt::GraphicsCaptureItem::Closed_revoker m_itemClosed; + std::shared_ptr m_frameWait; + + winrt::Streams::IRandomAccessStream m_stream{ nullptr }; + + // WIC components for GIF encoding + winrt::com_ptr m_wicFactory; + winrt::com_ptr m_wicStream; + winrt::com_ptr m_gifEncoder; + winrt::com_ptr m_encoderMetadataWriter; + + std::atomic m_isRecording = false; + std::atomic 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; +}; diff --git a/src/modules/ZoomIt/ZoomIt/ZoomIt.rc b/src/modules/ZoomIt/ZoomIt/ZoomIt.rc index 382c93208e..5bad5e7670 100644 --- a/src/modules/ZoomIt/ZoomIt/ZoomIt.rc +++ b/src/modules/ZoomIt/ZoomIt/ZoomIt.rc @@ -32,18 +32,18 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US // TEXTINCLUDE // -1 TEXTINCLUDE +1 TEXTINCLUDE BEGIN "resource.h\0" END -2 TEXTINCLUDE +2 TEXTINCLUDE BEGIN "#include ""winres.h""\r\n" "\0" END -3 TEXTINCLUDE +3 TEXTINCLUDE BEGIN "#include ""binres.rc""\0" END @@ -121,8 +121,8 @@ FONT 8, "MS Shell Dlg", 0, 0, 0x0 BEGIN DEFPUSHBUTTON "OK",IDOK,166,306,50,14 PUSHBUTTON "Cancel",IDCANCEL,223,306,50,14 - LTEXT "ZoomIt v9.10",IDC_VERSION,42,7,73,10 - LTEXT "Copyright 2006-2025 Mark Russinovich",IDC_COPYRIGHT,42,17,231,8 + LTEXT "ZoomIt v9.20",IDC_VERSION,42,7,73,10 + LTEXT "Copyright 2006-2025 Mark Russinovich",IDC_COPYRIGHT,42,17,231,8 CONTROL "Sysinternals - www.sysinternals.com",IDC_LINK, "SysLink",WS_TABSTOP,42,26,150,9 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 "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 + 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 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 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 - COMBOBOX IDC_MICROPHONE,81,152,172,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP - LTEXT "Microphone:",IDC_STATIC,32,154,47,8 + CONTROL "&Capture audio input:",IDC_CAPTURE_AUDIO,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,149,83,10 + COMBOBOX IDC_MICROPHONE,81,164,172,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Microphone:",IDC_STATIC,32,166,47,8 END SNIP DIALOGEX 0, 0, 260, 68 diff --git a/src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj b/src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj index f12898dbd4..0907ff3554 100644 --- a/src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj +++ b/src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj @@ -234,6 +234,7 @@ NotUsing NotUsing + Use @@ -288,6 +289,7 @@ + diff --git a/src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj.filters b/src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj.filters index 1754412d2d..e0416fe585 100644 --- a/src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj.filters +++ b/src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj.filters @@ -54,6 +54,9 @@ Source Files + + Source Files + @@ -95,6 +98,9 @@ Header Files + + Header Files + diff --git a/src/modules/ZoomIt/ZoomIt/ZoomItSettings.h b/src/modules/ZoomIt/ZoomIt/ZoomItSettings.h index 96e2f19e5b..486d5a61e7 100644 --- a/src/modules/ZoomIt/ZoomIt/ZoomItSettings.h +++ b/src/modules/ZoomIt/ZoomIt/ZoomItSettings.h @@ -3,6 +3,13 @@ #include "Registry.h" #include "DemoType.h" +// Recording format enum +enum class RecordingFormat +{ + GIF = 0, + MP4 = 1 +}; + DWORD g_ToggleKey = (HOTKEYF_CONTROL << 8)| '1'; DWORD g_LiveZoomToggleKey = ((HOTKEYF_CONTROL) << 8)| '4'; DWORD g_DrawToggleKey = ((HOTKEYF_CONTROL) << 8)| '2'; @@ -38,8 +45,10 @@ BOOLEAN g_DemoTypeUserDriven = false; TCHAR g_DemoTypeFile[MAX_PATH] = {0}; DWORD g_DemoTypeSpeedSlider = static_cast(((MIN_TYPING_SPEED - MAX_TYPING_SPEED) / 2) + MAX_TYPING_SPEED); 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; TCHAR g_MicrophoneDeviceId[MAX_PATH] = {0}; @@ -79,7 +88,9 @@ REG_SETTING RegSettings[] = { { L"ZoominSliderLevel", SETTING_TYPE_DWORD, 0, &g_SliderZoomLevel, static_cast(g_SliderZoomLevel) }, { L"Font", SETTING_TYPE_BINARY, sizeof g_LogFont, &g_LogFont, static_cast(0) }, { L"RecordFrameRate", SETTING_TYPE_DWORD, 0, &g_RecordFrameRate, static_cast(g_RecordFrameRate) }, - { L"RecordScaling", SETTING_TYPE_DWORD, 0, &g_RecordScaling, static_cast(g_RecordScaling) }, + { L"RecordingFormat", SETTING_TYPE_DWORD, 0, &g_RecordingFormat, static_cast(0) }, + { L"RecordScalingGIF", SETTING_TYPE_DWORD, 0, &g_RecordScalingGIF, static_cast(g_RecordScalingGIF) }, + { L"RecordScalingMP4", SETTING_TYPE_DWORD, 0, &g_RecordScalingMP4, static_cast(g_RecordScalingMP4) }, { L"CaptureAudio", SETTING_TYPE_BOOLEAN, 0, &g_CaptureAudio, static_cast(g_CaptureAudio) }, { L"MicrophoneDeviceId", SETTING_TYPE_STRING, sizeof(g_MicrophoneDeviceId), g_MicrophoneDeviceId, static_cast(0) }, { NULL, SETTING_TYPE_DWORD, 0, NULL, static_cast(0) } diff --git a/src/modules/ZoomIt/ZoomIt/Zoomit.cpp b/src/modules/ZoomIt/ZoomIt/Zoomit.cpp index fadc760339..12faf50fd4 100644 --- a/src/modules/ZoomIt/ZoomIt/Zoomit.cpp +++ b/src/modules/ZoomIt/ZoomIt/Zoomit.cpp @@ -15,6 +15,7 @@ #include "Utility.h" #include "WindowsVersions.h" #include "ZoomItSettings.h" +#include "GifRecordingSession.h" #ifdef __ZOOMIT_POWERTOYS__ #include @@ -68,6 +69,8 @@ COLORREF g_CustomColors[16]; #define SNIP_SAVE_HOTKEY 9 #define DEMOTYPE_HOTKEY 10 #define DEMOTYPE_RESET_HOTKEY 11 +#define RECORD_GIF_HOTKEY 12 +#define RECORD_GIF_WINDOW_HOTKEY 13 #define ZOOM_PAGE 0 #define LIVE_PAGE 1 @@ -89,6 +92,11 @@ OPTION_TABS g_OptionsTabs[] = { { _T("Snip"), NULL } }; +static const TCHAR* g_RecordingFormats[] = { + _T("GIF"), + _T("MP4") +}; + float g_ZoomLevels[] = { 1.25, 1.50, @@ -99,6 +107,8 @@ float g_ZoomLevels[] = { }; DWORD g_FramerateOptions[] = { + 15, + 24, 30, 60 }; @@ -120,7 +130,7 @@ const float STRONG_BLUR_RADIUS = 40; DWORD g_ToggleMod; DWORD g_LiveZoomToggleMod; DWORD g_DrawToggleMod; -DWORD g_BreakToggleMod; +DWORD g_BreakToggleMod; DWORD g_DemoTypeToggleMod; DWORD g_RecordToggleMod; DWORD g_SnipToggleMod; @@ -152,12 +162,15 @@ BOOLEAN g_running = TRUE; // Screen recording globals #define DEFAULT_RECORDING_FILE L"Recording.mp4" +#define DEFAULT_GIF_RECORDING_FILE L"Recording.gif" + BOOL g_RecordToggle = FALSE; BOOL g_RecordCropping = FALSE; SelectRectangle g_SelectRectangle; std::wstring g_RecordingSaveLocation; winrt::IDirect3DDevice g_RecordDevice{ nullptr }; std::shared_ptr g_RecordingSession = nullptr; +std::shared_ptr g_GifRecordingSession = nullptr; type_pGetMonitorInfo pGetMonitorInfo; type_MonitorFromPoint pMonitorFromPoint; @@ -192,7 +205,7 @@ ComputerGraphicsInit g_GraphicsInit; //---------------------------------------------------------------------------- // -// Saves specified filePath to clipboard. +// Saves specified filePath to clipboard. // //---------------------------------------------------------------------------- bool SaveToClipboard( const WCHAR* filePath, HWND hwnd ) @@ -207,18 +220,18 @@ bool SaveToClipboard( const WCHAR* filePath, HWND hwnd ) HDROP hDrop = static_cast(GlobalAlloc( GHND, size )); if (hDrop == NULL) { - return false; + return false; } DROPFILES* dFiles = static_cast(GlobalLock( hDrop )); if (dFiles == NULL) { GlobalFree( hDrop ); - return false; + return false; } dFiles->pFiles = sizeof(DROPFILES); - dFiles->fWide = TRUE; + dFiles->fWide = TRUE; wcscpy( reinterpret_cast(& dFiles[1]), filePath); GlobalUnlock( hDrop ); @@ -345,7 +358,7 @@ VOID ErrorDialog( HWND hParent, PCTSTR message, DWORD _Error ) TCHAR errmsg[1024]; FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, - NULL, _Error, + NULL, _Error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), reinterpret_cast(&lpMsgBuf), 0, NULL ); _stprintf( errmsg, L"%s: %s", message, lpMsgBuf ); @@ -391,7 +404,7 @@ VOID ErrorDialogString( HWND hParent, PCTSTR Message, const wchar_t *_Error ) // SetAutostartFilePath // // Sets the file path for later autostart config. -// +// //-------------------------------------------------------------------- void SetAutostartFilePath() { @@ -417,32 +430,32 @@ void SetAutostartFilePath() // ConfigureAutostart // // Enables or disables Zoomit autostart for the current image file. -// +// //-------------------------------------------------------------------- -bool ConfigureAutostart( HWND hParent, bool Enable ) +bool ConfigureAutostart( HWND hParent, bool Enable ) { HKEY hRunKey, hZoomit; DWORD error, length, type; TCHAR imageFile[MAX_PATH]; - error = RegOpenKeyEx( HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Run", + error = RegOpenKeyEx( HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Run", 0, KEY_SET_VALUE, &hRunKey ); if( error == ERROR_SUCCESS ) { if( Enable ) { - - error = RegOpenKeyEx( HKEY_CURRENT_USER, _T("Software\\Sysinternals\\Zoomit"), 0, + + error = RegOpenKeyEx( HKEY_CURRENT_USER, _T("Software\\Sysinternals\\Zoomit"), 0, KEY_QUERY_VALUE, &hZoomit ); if( error == ERROR_SUCCESS ) { length = sizeof(imageFile); #ifdef _WIN64 // Unconditionally reset filepath in case this was already set by 32 bit version - SetAutostartFilePath(); + SetAutostartFilePath(); #endif error = RegQueryValueEx( hZoomit, _T( "Filepath" ), 0, &type, (BYTE *) imageFile, &length ); RegCloseKey( hZoomit ); - if( error == ERROR_SUCCESS ) { + if( error == ERROR_SUCCESS ) { error = RegSetValueEx( hRunKey, APPNAME, 0, REG_SZ, (BYTE *) imageFile, static_cast(_tcslen(imageFile)+1) * sizeof(TCHAR)); @@ -454,7 +467,7 @@ bool ConfigureAutostart( HWND hParent, bool Enable ) if( error == ERROR_FILE_NOT_FOUND ) error = ERROR_SUCCESS; } RegCloseKey( hRunKey ); - } + } if( error != ERROR_SUCCESS ) { ErrorDialog( hParent, L"Error configuring auto start", error ); @@ -468,15 +481,15 @@ bool ConfigureAutostart( HWND hParent, bool Enable ) // IsAutostartConfigured // // Is this version of zoomit configured to autostart. -// +// //-------------------------------------------------------------------- bool IsAutostartConfigured() { HKEY hRunKey; - TCHAR imageFile[MAX_PATH]; + TCHAR imageFile[MAX_PATH]; DWORD error, imageFileLength, type; - error = RegOpenKeyEx( HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Run", + error = RegOpenKeyEx( HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Run", 0, KEY_QUERY_VALUE, &hRunKey ); if( error == ERROR_SUCCESS ) { @@ -496,15 +509,15 @@ bool IsAutostartConfigured() // // Returns true if this is the 32-bit version of the executable // and we're on 64-bit Windows. -// +// //-------------------------------------------------------------------- typedef BOOL (__stdcall *P_IS_WOW64PROCESS)( HANDLE hProcess, PBOOL Wow64Process ); -BOOL +BOOL RunningOnWin64( - VOID + VOID ) { P_IS_WOW64PROCESS pIsWow64Process; @@ -513,9 +526,9 @@ RunningOnWin64( pIsWow64Process = (P_IS_WOW64PROCESS) GetProcAddress(GetModuleHandle(_T("kernel32.dll")), "IsWow64Process"); if( pIsWow64Process ) { - + pIsWow64Process( GetCurrentProcess(), &isWow64 ); - } + } return isWow64; } @@ -524,7 +537,7 @@ RunningOnWin64( // // ExtractImageResource // -// Extracts the specified file that is located in a resource for +// Extracts the specified file that is located in a resource for // this executable. // //-------------------------------------------------------------------- @@ -532,15 +545,15 @@ BOOLEAN ExtractImageResource( PTCHAR ResourceName, PTCHAR TargetFile ) { HRSRC hResource; HGLOBAL hImageResource; - DWORD dwImageSize; + DWORD dwImageSize; LPVOID lpvImage; FILE *hFile; // Locate the resource - hResource = FindResource( NULL, ResourceName, _T("BINRES") ); - if( !hResource ) + hResource = FindResource( NULL, ResourceName, _T("BINRES") ); + if( !hResource ) return FALSE; - + hImageResource = LoadResource( NULL, hResource ); dwImageSize = SizeofResource( NULL, hResource ); lpvImage = LockResource( hImageResource ); @@ -562,10 +575,10 @@ BOOLEAN ExtractImageResource( PTCHAR ResourceName, PTCHAR TargetFile ) // // Returns true if this is the 32-bit version of the executable // and we're on 64-bit Windows. -// +// //-------------------------------------------------------------------- -DWORD -Run64bitVersion( +DWORD +Run64bitVersion( void ) { @@ -636,13 +649,13 @@ BOOLEAN IsPresentationMode() //---------------------------------------------------------------------------- // // EnableDisableSecondaryDisplay -// +// // Creates a second display on the secondary monitor for displaying the -// break timer. +// break timer. // //---------------------------------------------------------------------------- -LONG EnableDisableSecondaryDisplay( HWND hWnd, BOOLEAN Enable, - PDEVMODE OriginalDevMode ) +LONG EnableDisableSecondaryDisplay( HWND hWnd, BOOLEAN Enable, + PDEVMODE OriginalDevMode ) { LONG result; DEVMODE devMode{}; @@ -654,7 +667,7 @@ LONG EnableDisableSecondaryDisplay( HWND hWnd, BOOLEAN Enable, // devMode.dmSize = sizeof(devMode); devMode.dmDriverExtra = 0; - EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, &devMode); + EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, &devMode); *OriginalDevMode = devMode; // @@ -667,7 +680,7 @@ LONG EnableDisableSecondaryDisplay( HWND hWnd, BOOLEAN Enable, DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFLAGS | - DM_DISPLAYFREQUENCY; + DM_DISPLAYFREQUENCY; result = ChangeDisplaySettingsEx( L"\\\\.\\DISPLAY2", &devMode, NULL, @@ -723,11 +736,11 @@ LONG EnableDisableSecondaryDisplay( HWND hWnd, BOOLEAN Enable, // GetLineBounds // // Gets the rectangle bounding a line, taking into account pen width -// +// //---------------------------------------------------------------------------- Gdiplus::Rect GetLineBounds( POINT p1, POINT p2, int penWidth ) { - Gdiplus::Rect rect( min(p1.x, p2.x), min(p1.y, p2.y), + Gdiplus::Rect rect( min(p1.x, p2.x), min(p1.y, p2.y), abs(p1.x - p2.x), abs( p1.y - p2.y)); rect.Inflate( penWidth, penWidth ); return rect; @@ -738,7 +751,7 @@ Gdiplus::Rect GetLineBounds( POINT p1, POINT p2, int penWidth ) // InvalidateGdiplusRect // // Invalidate portion of window specified by Gdiplus::Rect -// +// //---------------------------------------------------------------------------- void InvalidateGdiplusRect(HWND hWnd, Gdiplus::Rect BoundsRect) { @@ -756,8 +769,8 @@ void InvalidateGdiplusRect(HWND hWnd, Gdiplus::Rect BoundsRect) // // CreateGdiplusBitmap // -// Creates a gdiplus bitmap of the specified region of the HDC. -// +// Creates a gdiplus bitmap of the specified region of the HDC. +// //---------------------------------------------------------------------------- Gdiplus::Bitmap *CreateGdiplusBitmap( HDC hDc, int x, int y, int Width, int Height ) { @@ -772,7 +785,7 @@ Gdiplus::Bitmap *CreateGdiplusBitmap( HDC hDc, int x, int y, int Width, int Heig Gdiplus::Bitmap *blurBitmap = new Gdiplus::Bitmap(hBitmap, NULL); DeleteDC(hdcNewBitmap); DeleteObject(hBitmap); - return blurBitmap; + return blurBitmap; } @@ -781,7 +794,7 @@ Gdiplus::Bitmap *CreateGdiplusBitmap( HDC hDc, int x, int y, int Width, int Heig // CreateBitmapMemoryDIB // // Creates a memory DC and DIB for the specified region of the screen. -// +// //---------------------------------------------------------------------------- BYTE* CreateBitmapMemoryDIB(HDC hdcScreenCompat, HDC hBitmapDc, Gdiplus::Rect* lineBounds, HDC* hdcMem, HBITMAP* hDIBOrig, HBITMAP* hPreviousBitmap) @@ -819,8 +832,8 @@ BYTE* CreateBitmapMemoryDIB(HDC hdcScreenCompat, HDC hBitmapDc, Gdiplus::Rect* l // // LockGdiPlusBitmap // -// Locks the Gdi+ bitmap so that we can access its pixels in memory. -// +// Locks the Gdi+ bitmap so that we can access its pixels in memory. +// //---------------------------------------------------------------------------- #ifdef _MSC_VER // Analyzers want us to use a scoped object instead of new. But given all the operations done in Bitmaps it seems better to leave it as a heap object. @@ -835,7 +848,7 @@ Gdiplus::BitmapData* LockGdiPlusBitmap(Gdiplus::Bitmap* Bitmap) Gdiplus::Rect lineBitmapBounds(0, 0, Bitmap->GetWidth(), Bitmap->GetHeight()); Bitmap->LockBits(&lineBitmapBounds, Gdiplus::ImageLockModeRead, Bitmap->GetPixelFormat(), lineData); - return lineData; + return lineData; } #ifdef _MSC_VER #pragma warning(pop) @@ -846,11 +859,11 @@ Gdiplus::BitmapData* LockGdiPlusBitmap(Gdiplus::Bitmap* Bitmap) // // BlurScreen // -// Blur the portion of the screen by copying a blurred bitmap with the -// specified shape. -// +// Blur the portion of the screen by copying a blurred bitmap with the +// specified shape. +// //---------------------------------------------------------------------------- -void BlurScreen(HDC hdcScreenCompat, Gdiplus::Rect* lineBounds, +void BlurScreen(HDC hdcScreenCompat, Gdiplus::Rect* lineBounds, Gdiplus::Bitmap *BlurBitmap, BYTE* pPixels) { HDC hdcDIB; @@ -896,8 +909,8 @@ void BlurScreen(HDC hdcScreenCompat, Gdiplus::Rect* lineBounds, // // BitmapBlur // -// Blurs the bitmap. -// +// Blurs the bitmap. +// //---------------------------------------------------------------------------- void BitmapBlur(Gdiplus::Bitmap* hBitmap) { @@ -928,7 +941,7 @@ void BitmapBlur(Gdiplus::Bitmap* hBitmap) // DrawBlurredShape // // Blur a shaped region of the screen. -// +// //---------------------------------------------------------------------------- void DrawBlurredShape( DWORD Shape, Gdiplus::Pen *pen, HDC hdcScreenCompat, Gdiplus::Graphics *dstGraphics, int x1, int y1, int x2, int y2) @@ -937,7 +950,7 @@ void DrawBlurredShape( DWORD Shape, Gdiplus::Pen *pen, HDC hdcScreenCompat, Gdip Gdiplus::Rect lineBounds( min( x1, x2 ), min( y1, y2 ), abs( x2 - x1 ), abs( y2 - y1 ) ); // Expand for line drawing - if (Shape == DRAW_LINE) + if (Shape == DRAW_LINE) lineBounds.Inflate( static_cast(g_PenWidth / 2), static_cast(g_PenWidth / 2) ); Gdiplus::Bitmap* lineBitmap = new Gdiplus::Bitmap(lineBounds.Width, lineBounds.Height, PixelFormat32bppARGB); @@ -978,7 +991,7 @@ void DrawBlurredShape( DWORD Shape, Gdiplus::Pen *pen, HDC hdcScreenCompat, Gdip // CreateDrawingBitmap // // Create a bitmap to draw on. -// +// //---------------------------------------------------------------------------- Gdiplus::Bitmap* CreateDrawingBitmap(Gdiplus::Rect lineBounds ) { @@ -992,8 +1005,8 @@ Gdiplus::Bitmap* CreateDrawingBitmap(Gdiplus::Rect lineBounds ) // // DrawBitmapLine // -// Creates a bitmap and draws a line on it. -// +// Creates a bitmap and draws a line on it. +// //---------------------------------------------------------------------------- Gdiplus::Bitmap* DrawBitmapLine(Gdiplus::Rect lineBounds, POINT p1, POINT p2, Gdiplus::Pen *pen) { @@ -1013,7 +1026,7 @@ Gdiplus::Bitmap* DrawBitmapLine(Gdiplus::Rect lineBounds, POINT p1, POINT p2, Gd // ColorFromColorRef // // Returns a color object from the colorRef that includes the alpha channel -// +// //---------------------------------------------------------------------------- Gdiplus::Color ColorFromColorRef(DWORD colorRef) { BYTE a = (colorRef >> 24) & 0xFF; // Extract the alpha channel value @@ -1028,8 +1041,8 @@ Gdiplus::Color ColorFromColorRef(DWORD colorRef) { // // AdjustHighlighterColor // -// Lighten the color. -// +// Lighten the color. +// //---------------------------------------------------------------------------- void AdjustHighlighterColor(BYTE* red, BYTE* green, BYTE* blue) { @@ -1044,8 +1057,8 @@ void AdjustHighlighterColor(BYTE* red, BYTE* green, BYTE* blue) { // BlendColors // // Blends two colors together using the alpha channel of the second color. -// The highlighter is the second color. -// +// The highlighter is the second color. +// //---------------------------------------------------------------------------- COLORREF BlendColors(COLORREF color1, const Gdiplus::Color& color2) { @@ -1068,7 +1081,7 @@ COLORREF BlendColors(COLORREF color1, const Gdiplus::Color& color2) { // int maxValue = max(red1, max(green1, blue1)); if(TRUE) { // red1 > 0x10 && red1 < 0xC0 && (maxValue - minValue < 0x40)) { - // This does a standard bright highlight + // This does a standard bright highlight alpha2 = 0; AdjustHighlighterColor( &red2, &green2, &blue2 ); redResult = red2 & red1; @@ -1095,7 +1108,7 @@ COLORREF BlendColors(COLORREF color1, const Gdiplus::Color& color2) { // Draws the shape with the highlighter color. // //---------------------------------------------------------------------------- -void DrawHighlightedShape( DWORD Shape, HDC hdcScreenCompat, Gdiplus::Brush *pBrush, +void DrawHighlightedShape( DWORD Shape, HDC hdcScreenCompat, Gdiplus::Brush *pBrush, Gdiplus::Pen *pPen, int x1, int y1, int x2, int y2) { // Create a new bitmap that's the size of the area covered by the line + 2 * g_PenWidth @@ -1115,7 +1128,7 @@ void DrawHighlightedShape( DWORD Shape, HDC hdcScreenCompat, Gdiplus::Brush *pBr break; case DRAW_ELLIPSE: lineGraphics.FillEllipse( pBrush, 0, 0, lineBounds.Width, lineBounds.Height); - break; + break; case DRAW_LINE: lineGraphics.DrawLine(pPen, x1 - lineBounds.X, y1 - lineBounds.Y, x2 - lineBounds.X, y2 - lineBounds.Y); break; @@ -1197,7 +1210,7 @@ void DrawHighlightedShape( DWORD Shape, HDC hdcScreenCompat, Gdiplus::Brush *pBr // // CreateFadedDesktopBackground // -// Creates a snapshot of the desktop that's faded and alpha blended with +// Creates a snapshot of the desktop that's faded and alpha blended with // black. // //---------------------------------------------------------------------------- @@ -1211,7 +1224,7 @@ HBITMAP CreateFadedDesktopBackground( HDC hdc, LPRECT rcScreen, LPRECT rcCrop ) HBITMAP hBitmap = CreateCompatibleBitmap( hdcScreen, width, height ); HBITMAP hOld = static_cast(SelectObject( hdcMem, hBitmap )); HBRUSH hBrush = CreateSolidBrush(RGB(0, 0, 0)); - + // start with black background FillRect( hdcMem, rcScreen, hBrush ); if(rcCrop != NULL && rcCrop->left != -1 ) { @@ -1227,8 +1240,8 @@ HBITMAP CreateFadedDesktopBackground( HDC hdc, LPRECT rcScreen, LPRECT rcCrop ) blend.BlendFlags = 0; blend.SourceConstantAlpha = 0x4F; blend.AlphaFormat = 0; - AlphaBlend( hdcMem,0, 0, width, height, - hdcScreen, rcScreen->left, rcScreen->top, + AlphaBlend( hdcMem,0, 0, width, height, + hdcScreen, rcScreen->left, rcScreen->top, width, height, blend ); SelectObject( hdcMem, hOld ); @@ -1249,9 +1262,9 @@ HBITMAP CreateFadedDesktopBackground( HDC hdc, LPRECT rcScreen, LPRECT rcCrop ) void AdjustToMoveBoundary( float zoomLevel, int *coordinate, int cursor, int size, int max ) { int diff = static_cast (static_cast(size)/ static_cast(LIVEZOOM_MOVE_REGIONS)); - if( cursor - *coordinate < diff ) - *coordinate = max( 0, cursor - diff ); - else if( (*coordinate + size) - cursor < diff ) + if( cursor - *coordinate < diff ) + *coordinate = max( 0, cursor - diff ); + else if( (*coordinate + size) - cursor < diff ) *coordinate = min( cursor + diff - size, max - size ); } @@ -1278,10 +1291,10 @@ void GetZoomedTopLeftCoordinates( float zoomLevel, POINT *cursorPos, int *x, int // // ScaleImage // -// Use gdi+ for anti-aliased bitmap stretching. +// Use gdi+ for anti-aliased bitmap stretching. // //---------------------------------------------------------------------------- -void ScaleImage( HDC hdcDst, float xDst, float yDst, float wDst, float hDst, +void ScaleImage( HDC hdcDst, float xDst, float yDst, float wDst, float hDst, HBITMAP bmSrc, float xSrc, float ySrc, float wSrc, float hSrc ) { Gdiplus::Graphics dstGraphics( hdcDst ); @@ -1331,7 +1344,7 @@ using namespace Gdiplus; *pClsid = pImageCodecInfo[j].Clsid; free(pImageCodecInfo); return j; // Success - } + } } free(pImageCodecInfo); @@ -1339,20 +1352,20 @@ using namespace Gdiplus; } //---------------------------------------------------------------------- -// +// // ConvertToUnicode // //---------------------------------------------------------------------- -void -ConvertToUnicode( - PCHAR aString, - PWCHAR wString, - DWORD wStringLength +void +ConvertToUnicode( + PCHAR aString, + PWCHAR wString, + DWORD wStringLength ) { size_t len; - len = MultiByteToWideChar( CP_ACP, 0, aString, static_cast(strlen(aString)), + len = MultiByteToWideChar( CP_ACP, 0, aString, static_cast(strlen(aString)), wString, wStringLength ); wString[len] = 0; } @@ -1411,14 +1424,14 @@ void EnableDisableTrayIcon( HWND hWnd, BOOLEAN Enable ) NOTIFYICONDATA tNotifyIconData; memset( &tNotifyIconData, 0, sizeof(tNotifyIconData)); - tNotifyIconData.cbSize = sizeof(NOTIFYICONDATA); - tNotifyIconData.hWnd = hWnd; - tNotifyIconData.uID = 1; - tNotifyIconData.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP; - tNotifyIconData.uCallbackMessage = WM_USER_TRAY_ACTIVATE; - tNotifyIconData.hIcon = LoadIcon( g_hInstance, L"APPICON" ); + tNotifyIconData.cbSize = sizeof(NOTIFYICONDATA); + tNotifyIconData.hWnd = hWnd; + tNotifyIconData.uID = 1; + tNotifyIconData.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP; + tNotifyIconData.uCallbackMessage = WM_USER_TRAY_ACTIVATE; + tNotifyIconData.hIcon = LoadIcon( g_hInstance, L"APPICON" ); lstrcpyn(tNotifyIconData.szTip, APPNAME, sizeof(APPNAME)); - Shell_NotifyIcon(Enable ? NIM_ADD : NIM_DELETE, &tNotifyIconData); + Shell_NotifyIcon(Enable ? NIM_ADD : NIM_DELETE, &tNotifyIconData); } //---------------------------------------------------------------------------- @@ -1426,7 +1439,7 @@ void EnableDisableTrayIcon( HWND hWnd, BOOLEAN Enable ) // EnableDisableOpacity // //---------------------------------------------------------------------------- -void EnableDisableOpacity( HWND hWnd, BOOLEAN Enable ) +void EnableDisableOpacity( HWND hWnd, BOOLEAN Enable ) { DWORD exStyle; @@ -1453,11 +1466,11 @@ void EnableDisableOpacity( HWND hWnd, BOOLEAN Enable ) // EnableDisableScreenSaver // //---------------------------------------------------------------------------- -void EnableDisableScreenSaver( BOOLEAN Enable ) +void EnableDisableScreenSaver( BOOLEAN Enable ) { - SystemParametersInfo(SPI_SETSCREENSAVEACTIVE,Enable,0,0); - SystemParametersInfo(SPI_SETPOWEROFFACTIVE,Enable,0,0); - SystemParametersInfo(SPI_SETLOWPOWERACTIVE,Enable,0,0); + SystemParametersInfo(SPI_SETSCREENSAVEACTIVE,Enable,0,0); + SystemParametersInfo(SPI_SETPOWEROFFACTIVE,Enable,0,0); + SystemParametersInfo(SPI_SETLOWPOWERACTIVE,Enable,0,0); } //---------------------------------------------------------------------------- @@ -1470,25 +1483,25 @@ void EnableDisableStickyKeys( BOOLEAN Enable ) static STICKYKEYS prevStickyKeyValue = {0}; STICKYKEYS newStickyKeyValue = {0}; - // Need to do this on Vista tablet to stop sticky key popup when you + // Need to do this on Vista tablet to stop sticky key popup when you // hold down the shift key and draw with the pen. if( Enable ) { if( prevStickyKeyValue.cbSize == sizeof(STICKYKEYS)) { - SystemParametersInfo(SPI_SETSTICKYKEYS, + SystemParametersInfo(SPI_SETSTICKYKEYS, sizeof(STICKYKEYS), &prevStickyKeyValue, SPIF_SENDCHANGE); } } else { prevStickyKeyValue.cbSize = sizeof(STICKYKEYS); - if (SystemParametersInfo(SPI_GETSTICKYKEYS, sizeof(STICKYKEYS), + if (SystemParametersInfo(SPI_GETSTICKYKEYS, sizeof(STICKYKEYS), &prevStickyKeyValue, 0)) { newStickyKeyValue.cbSize = sizeof(STICKYKEYS); newStickyKeyValue.dwFlags = 0; - if( !SystemParametersInfo(SPI_SETSTICKYKEYS, + if( !SystemParametersInfo(SPI_SETSTICKYKEYS, sizeof(STICKYKEYS), &newStickyKeyValue, SPIF_SENDCHANGE)) { // DWORD error = GetLastError(); @@ -1520,7 +1533,7 @@ constexpr DWORD GetKeyMod( DWORD Key ) // AdvancedBreakProc // //---------------------------------------------------------------------------- -INT_PTR CALLBACK AdvancedBreakProc( HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam ) +INT_PTR CALLBACK AdvancedBreakProc( HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam ) { TCHAR opacity[10]; static TCHAR newSoundFile[MAX_PATH]; @@ -1535,9 +1548,9 @@ INT_PTR CALLBACK AdvancedBreakProc( HWND hDlg, UINT message, WPARAM wParam, LPAR pSHAutoComplete( GetDlgItem( hDlg, IDC_SOUND_FILE), SHACF_FILESYSTEM ); pSHAutoComplete( GetDlgItem( hDlg, IDC_BACKGROUND_FILE), SHACF_FILESYSTEM ); } - CheckDlgButton( hDlg, IDC_CHECK_BACKGROUND_FILE, + CheckDlgButton( hDlg, IDC_CHECK_BACKGROUND_FILE, g_BreakShowBackgroundFile ? BST_CHECKED: BST_UNCHECKED ); - CheckDlgButton( hDlg, IDC_CHECK_SOUND_FILE, + CheckDlgButton( hDlg, IDC_CHECK_SOUND_FILE, g_BreakPlaySoundFile ? BST_CHECKED: BST_UNCHECKED ); CheckDlgButton( hDlg, IDC_CHECK_SHOW_EXPIRED, g_ShowExpiredTime ? BST_CHECKED : BST_UNCHECKED ); @@ -1575,7 +1588,7 @@ INT_PTR CALLBACK AdvancedBreakProc( HWND hDlg, UINT message, WPARAM wParam, LPAR EnableWindow( GetDlgItem( hDlg, IDC_BACKGROUND_BROWSE ), FALSE ); EnableWindow( GetDlgItem( hDlg, IDC_CHECK_BACKGROUND_STRETCH ), FALSE ); } - CheckDlgButton( hDlg, + CheckDlgButton( hDlg, g_BreakShowDesktop ? IDC_STATIC_DESKTOP_BACKGROUND : IDC_STATIC_BACKGROUND_FILE, BST_CHECKED ); _tcscpy( newBackgroundFile, g_BreakBackgroundFile ); SetDlgItemText( hDlg, IDC_BACKGROUND_FILE, g_BreakBackgroundFile ); @@ -1585,10 +1598,10 @@ INT_PTR CALLBACK AdvancedBreakProc( HWND hDlg, UINT message, WPARAM wParam, LPAR for( i = 10; i <= 100; i += 10) { _stprintf( opacity, L"%d%%", i ); - SendMessage( GetDlgItem( hDlg, IDC_OPACITY ), CB_ADDSTRING, 0, + SendMessage( GetDlgItem( hDlg, IDC_OPACITY ), CB_ADDSTRING, 0, reinterpret_cast(opacity)); } - SendMessage( GetDlgItem( hDlg, IDC_OPACITY ), CB_SETCURSEL, + SendMessage( GetDlgItem( hDlg, IDC_OPACITY ), CB_SETCURSEL, g_BreakOpacity / 10 - 1, 0 ); return TRUE; @@ -1597,25 +1610,25 @@ INT_PTR CALLBACK AdvancedBreakProc( HWND hDlg, UINT message, WPARAM wParam, LPAR case BN_CLICKED: if( LOWORD( wParam ) == IDC_CHECK_SOUND_FILE ) { - EnableWindow( GetDlgItem( hDlg, IDC_STATIC_SOUND_FILE ), + EnableWindow( GetDlgItem( hDlg, IDC_STATIC_SOUND_FILE ), IsDlgButtonChecked( hDlg, IDC_CHECK_SOUND_FILE) == BST_CHECKED ); - EnableWindow( GetDlgItem( hDlg, IDC_SOUND_FILE ), + EnableWindow( GetDlgItem( hDlg, IDC_SOUND_FILE ), + IsDlgButtonChecked( hDlg, IDC_CHECK_SOUND_FILE) == BST_CHECKED ); + EnableWindow( GetDlgItem( hDlg, IDC_SOUND_BROWSE ), IsDlgButtonChecked( hDlg, IDC_CHECK_SOUND_FILE) == BST_CHECKED ); - EnableWindow( GetDlgItem( hDlg, IDC_SOUND_BROWSE ), - IsDlgButtonChecked( hDlg, IDC_CHECK_SOUND_FILE) == BST_CHECKED ); } if( LOWORD( wParam ) == IDC_CHECK_BACKGROUND_FILE ) { - EnableWindow( GetDlgItem( hDlg, IDC_CHECK_BACKGROUND_STRETCH ), + EnableWindow( GetDlgItem( hDlg, IDC_CHECK_BACKGROUND_STRETCH ), IsDlgButtonChecked( hDlg, IDC_CHECK_BACKGROUND_FILE) == BST_CHECKED ); - EnableWindow( GetDlgItem( hDlg, IDC_STATIC_DESKTOP_BACKGROUND ), + EnableWindow( GetDlgItem( hDlg, IDC_STATIC_DESKTOP_BACKGROUND ), IsDlgButtonChecked( hDlg, IDC_CHECK_BACKGROUND_FILE) == BST_CHECKED ); - EnableWindow( GetDlgItem( hDlg, IDC_STATIC_BACKGROUND_FILE ), + EnableWindow( GetDlgItem( hDlg, IDC_STATIC_BACKGROUND_FILE ), IsDlgButtonChecked( hDlg, IDC_CHECK_BACKGROUND_FILE) == BST_CHECKED ); - EnableWindow( GetDlgItem( hDlg, IDC_BACKGROUND_FILE ), + EnableWindow( GetDlgItem( hDlg, IDC_BACKGROUND_FILE ), + IsDlgButtonChecked( hDlg, IDC_CHECK_BACKGROUND_FILE) == BST_CHECKED ); + EnableWindow( GetDlgItem( hDlg, IDC_BACKGROUND_BROWSE ), IsDlgButtonChecked( hDlg, IDC_CHECK_BACKGROUND_FILE) == BST_CHECKED ); - EnableWindow( GetDlgItem( hDlg, IDC_BACKGROUND_BROWSE ), - IsDlgButtonChecked( hDlg, IDC_CHECK_BACKGROUND_FILE) == BST_CHECKED ); } break; } @@ -1704,7 +1717,7 @@ INT_PTR CALLBACK AdvancedBreakProc( HWND hDlg, UINT message, WPARAM wParam, LPAR #endif if( g_BreakPlaySoundFile && GetFileAttributes( newSoundFile ) == -1 ) { - MessageBox( hDlg, L"The specified sound file is inaccessible", + MessageBox( hDlg, L"The specified sound file is inaccessible", L"Advanced Break Options Error", MB_ICONERROR ); break; } @@ -1715,7 +1728,7 @@ INT_PTR CALLBACK AdvancedBreakProc( HWND hDlg, UINT message, WPARAM wParam, LPAR if( !g_BreakShowDesktop && g_BreakShowBackgroundFile && GetFileAttributes( newBackgroundFile ) == -1 ) { - MessageBox( hDlg, L"The specified background file is inaccessible", + MessageBox( hDlg, L"The specified background file is inaccessible", L"Advanced Break Options Error", MB_ICONERROR ); break; } @@ -1729,7 +1742,7 @@ INT_PTR CALLBACK AdvancedBreakProc( HWND hDlg, UINT message, WPARAM wParam, LPAR break; } } - GetDlgItemText( hDlg, IDC_OPACITY, opacity, sizeof(opacity)/sizeof(opacity[0])); + GetDlgItemText( hDlg, IDC_OPACITY, opacity, sizeof(opacity)/sizeof(opacity[0])); _stscanf( opacity, L"%d%%", &g_BreakOpacity ); reg.WriteRegSettings( RegSettings ); EndDialog(hDlg, 0); @@ -1753,14 +1766,14 @@ INT_PTR CALLBACK AdvancedBreakProc( HWND hDlg, UINT message, WPARAM wParam, LPAR // OptionsTabProc // //---------------------------------------------------------------------------- -INT_PTR CALLBACK OptionsTabProc( HWND hDlg, UINT message, - WPARAM wParam, LPARAM lParam ) +INT_PTR CALLBACK OptionsTabProc( HWND hDlg, UINT message, + WPARAM wParam, LPARAM lParam ) { HDC hDC; LOGFONT lf; CHOOSEFONT chooseFont; HFONT hFont; - PAINTSTRUCT ps; + PAINTSTRUCT ps; HWND hTextPreview; HDC hDc; RECT previewRc; @@ -1771,6 +1784,58 @@ INT_PTR CALLBACK OptionsTabProc( HWND hDlg, UINT message, case WM_INITDIALOG: return TRUE; case WM_COMMAND: + // Handle combo box selection changes + if (HIWORD(wParam) == CBN_SELCHANGE) { + if (LOWORD(wParam) == IDC_RECORD_SCALING) { + + int format = static_cast(SendMessage(GetDlgItem(hDlg, IDC_RECORD_FORMAT), CB_GETCURSEL, 0, 0)); + int scale = static_cast(SendMessage(GetDlgItem(hDlg, IDC_RECORD_SCALING), CB_GETCURSEL, 0, 0)); + if(format == 0) + { + g_RecordScalingGIF = static_cast((scale + 1) * 10); + } + else + { + g_RecordScalingMP4 = static_cast((scale + 1) * 10); + } + } + else if (LOWORD(wParam) == IDC_RECORD_FORMAT) { + // Get the currently selected format + int selection = static_cast(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(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(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 )) { case IDC_ADVANCED_BREAK: DialogBox( g_hInstance, L"ADVANCED_BREAK", hDlg, AdvancedBreakProc ); @@ -1785,7 +1850,7 @@ INT_PTR CALLBACK OptionsTabProc( HWND hDlg, UINT message, chooseFont.hwndOwner = hDlg; chooseFont.lpLogFont = &lf; chooseFont.Flags = CF_SCREENFONTS|CF_ENABLETEMPLATE| - CF_INITTOLOGFONTSTRUCT|CF_LIMITSIZE; + CF_INITTOLOGFONTSTRUCT|CF_LIMITSIZE; chooseFont.rgbColors = RGB (0, 0, 0); chooseFont.lCustData = 0; chooseFont.nSizeMin = 16; @@ -1811,7 +1876,7 @@ INT_PTR CALLBACK OptionsTabProc( HWND hDlg, UINT message, openFileName.nFilterIndex = 1; openFileName.lpstrFilter = L"All Files\0*.*\0\0"; openFileName.lpstrFile = filePath; - + if( GetOpenFileName( &openFileName ) ) { if( GetFileAttributes( filePath ) == -1 ) @@ -1835,20 +1900,20 @@ INT_PTR CALLBACK OptionsTabProc( HWND hDlg, UINT message, LOGFONT _lf = g_LogFont; _lf.lfHeight = -21; hFont = CreateFontIndirect( &_lf); - hDc = BeginPaint(hDlg, &ps); + hDc = BeginPaint(hDlg, &ps); SelectObject( hDc, hFont ); GetWindowRect( hTextPreview, &previewRc ); - MapWindowPoints( NULL, hDlg, reinterpret_cast(&previewRc), 2); + MapWindowPoints( NULL, hDlg, reinterpret_cast(&previewRc), 2); previewRc.top += 6; - DrawText( hDc, L"Sample", static_cast(_tcslen(L"Sample")), &previewRc, + DrawText( hDc, L"Sample", static_cast(_tcslen(L"Sample")), &previewRc, DT_CENTER|DT_VCENTER|DT_SINGLELINE ); EndPaint( hDlg, &ps ); DeleteObject( hFont ); } - break; + break; default: break; } @@ -1861,7 +1926,7 @@ INT_PTR CALLBACK OptionsTabProc( HWND hDlg, UINT message, // OptionsAddTabs // //---------------------------------------------------------------------------- -VOID OptionsAddTabs( HWND hOptionsDlg, HWND hTabCtrl ) +VOID OptionsAddTabs( HWND hOptionsDlg, HWND hTabCtrl ) { int i; TCITEM tcItem; @@ -1873,14 +1938,14 @@ VOID OptionsAddTabs( HWND hOptionsDlg, HWND hTabCtrl ) tcItem.mask = TCIF_TEXT; tcItem.pszText = g_OptionsTabs[i].TabTitle; TabCtrl_InsertItem( hTabCtrl, i, &tcItem ); - g_OptionsTabs[i].hPage = CreateDialog( g_hInstance, g_OptionsTabs[i].TabTitle, + g_OptionsTabs[i].hPage = CreateDialog( g_hInstance, g_OptionsTabs[i].TabTitle, hOptionsDlg, OptionsTabProc ); } TabCtrl_AdjustRect( hTabCtrl, FALSE, &rc ); for( i = 0; i < sizeof( g_OptionsTabs )/sizeof(g_OptionsTabs[0]); i++ ) { pageRc = rc; - MapWindowPoints( NULL, g_OptionsTabs[i].hPage, reinterpret_cast(&pageRc), 2); + MapWindowPoints( NULL, g_OptionsTabs[i].hPage, reinterpret_cast(&pageRc), 2); SetWindowPos( g_OptionsTabs[i].hPage, HWND_TOP, @@ -1914,6 +1979,8 @@ void UnregisterAllHotkeys( HWND hWnd ) UnregisterHotKey( hWnd, SNIP_SAVE_HOTKEY); UnregisterHotKey( hWnd, DEMOTYPE_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_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); } @@ -1996,8 +2066,8 @@ void UpdateDrawTabHeaderFont() // OptionsProc // //---------------------------------------------------------------------------- -INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message, - WPARAM wParam, LPARAM lParam ) +INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message, + WPARAM wParam, LPARAM lParam ) { static HFONT hFontBold = nullptr; PNMLINK notify = nullptr; @@ -2026,7 +2096,7 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message, SetForegroundWindow( hDlg ); SetActiveWindow( hDlg ); - SetWindowPos( hDlg, HWND_TOP, 0, 0, 0, 0, SWP_NOSIZE|SWP_NOMOVE|SWP_SHOWWINDOW ); + SetWindowPos( hDlg, HWND_TOP, 0, 0, 0, 0, SWP_NOSIZE|SWP_NOMOVE|SWP_SHOWWINDOW ); #if 1 // set version info TCHAR filePath[MAX_PATH]; @@ -2054,9 +2124,9 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message, UpdateDrawTabHeaderFont(); // Configure options - SendMessage( GetDlgItem( g_OptionsTabs[ZOOM_PAGE].hPage, IDC_HOTKEY), HKM_SETRULES, - static_cast(HKCOMB_NONE), // invalid key combinations - MAKELPARAM(HOTKEYF_ALT, 0)); // add ALT to invalid entries + SendMessage( GetDlgItem( g_OptionsTabs[ZOOM_PAGE].hPage, IDC_HOTKEY), HKM_SETRULES, + static_cast(HKCOMB_NONE), // invalid key combinations + MAKELPARAM(HOTKEYF_ALT, 0)); // add ALT to invalid entries if( g_ToggleKey ) SendMessage( GetDlgItem( g_OptionsTabs[ZOOM_PAGE].hPage, IDC_HOTKEY), HKM_SETHOTKEY, g_ToggleKey, 0 ); if( pMagInitialize ) { @@ -2074,13 +2144,13 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message, if( g_DemoTypeToggleKey ) SendMessage( GetDlgItem( g_OptionsTabs[DEMOTYPE_PAGE].hPage, IDC_DEMOTYPE_HOTKEY ), HKM_SETHOTKEY, g_DemoTypeToggleKey, 0 ); if( g_RecordToggleKey ) SendMessage( GetDlgItem( g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_HOTKEY), HKM_SETHOTKEY, g_RecordToggleKey, 0 ); if( g_SnipToggleKey) SendMessage( GetDlgItem( g_OptionsTabs[SNIP_PAGE].hPage, IDC_SNIP_HOTKEY), HKM_SETHOTKEY, g_SnipToggleKey, 0 ); - CheckDlgButton( hDlg, IDC_SHOW_TRAY_ICON, + CheckDlgButton( hDlg, IDC_SHOW_TRAY_ICON, g_ShowTrayIcon ? BST_CHECKED: BST_UNCHECKED ); - CheckDlgButton( hDlg, IDC_AUTOSTART, + CheckDlgButton( hDlg, IDC_AUTOSTART, IsAutostartConfigured() ? BST_CHECKED: BST_UNCHECKED ); - CheckDlgButton( g_OptionsTabs[ZOOM_PAGE].hPage, IDC_ANIMATE_ZOOM, + CheckDlgButton( g_OptionsTabs[ZOOM_PAGE].hPage, IDC_ANIMATE_ZOOM, g_AnimateZoom ? BST_CHECKED: BST_UNCHECKED ); - CheckDlgButton( g_OptionsTabs[ZOOM_PAGE].hPage, IDC_SMOOTH_IMAGE, + CheckDlgButton( g_OptionsTabs[ZOOM_PAGE].hPage, IDC_SMOOTH_IMAGE, g_SmoothImage ? BST_CHECKED: BST_UNCHECKED ); SendMessage( GetDlgItem(g_OptionsTabs[ZOOM_PAGE].hPage, IDC_ZOOM_SLIDER), TBM_SETRANGE, false, MAKELONG(0,_countof(g_ZoomLevels)-1) ); @@ -2089,18 +2159,18 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message, _stprintf( text, L"%d", g_PenWidth ); SetDlgItemText( g_OptionsTabs[DRAW_PAGE].hPage, IDC_PEN_WIDTH, text ); SendMessage( GetDlgItem( g_OptionsTabs[DRAW_PAGE].hPage, IDC_PEN_WIDTH ), EM_LIMITTEXT, 1, 0 ); - SendMessage (GetDlgItem( g_OptionsTabs[DRAW_PAGE].hPage, IDC_SPIN), UDM_SETRANGE, 0L, + SendMessage (GetDlgItem( g_OptionsTabs[DRAW_PAGE].hPage, IDC_SPIN), UDM_SETRANGE, 0L, MAKELPARAM (19, 1)); _stprintf( text, L"%d", g_BreakTimeout ); SetDlgItemText( g_OptionsTabs[BREAK_PAGE].hPage, IDC_TIMER, text ); SendMessage( GetDlgItem( g_OptionsTabs[BREAK_PAGE].hPage, IDC_TIMER ), EM_LIMITTEXT, 2, 0 ); - SendMessage (GetDlgItem( g_OptionsTabs[BREAK_PAGE].hPage, IDC_SPIN_TIMER), UDM_SETRANGE, 0L, + SendMessage (GetDlgItem( g_OptionsTabs[BREAK_PAGE].hPage, IDC_SPIN_TIMER), UDM_SETRANGE, 0L, MAKELPARAM (99, 1)); CheckDlgButton( g_OptionsTabs[BREAK_PAGE].hPage, IDC_CHECK_SHOW_EXPIRED, g_ShowExpiredTime ? BST_CHECKED : BST_UNCHECKED ); - CheckDlgButton( g_OptionsTabs[RECORD_PAGE].hPage, IDC_CAPTURE_AUDIO, + CheckDlgButton( g_OptionsTabs[RECORD_PAGE].hPage, IDC_CAPTURE_AUDIO, g_CaptureAudio ? BST_CHECKED: BST_UNCHECKED ); for (int i = 0; i < _countof(g_FramerateOptions); i++) { @@ -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(i), static_cast(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(CB_ADDSTRING), static_cast(0), reinterpret_cast(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(selection), static_cast(0) ); + for(unsigned int i = 1; i < 11; i++) { _stprintf(text, L"%2.1f", (static_cast(i)) / 10 ); SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_SCALING), static_cast(CB_ADDSTRING), static_cast(0), reinterpret_cast(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(i)-1, static_cast(0)); + } + if (g_RecordingFormat == RecordingFormat::MP4 && i*10 == g_RecordScalingMP4 ) { SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_SCALING), CB_SETCURSEL, static_cast(i)-1, static_cast(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 SendMessage( GetDlgItem( g_OptionsTabs[RECORD_PAGE].hPage, IDC_MICROPHONE ), static_cast(CB_ADDSTRING), static_cast(0), reinterpret_cast(L"Default")); - size_t selection = 0; + selection = 0; for( size_t i = 0; i < microphones.size(); i++ ) { SendMessage( GetDlgItem( g_OptionsTabs[RECORD_PAGE].hPage, IDC_MICROPHONE ), static_cast(CB_ADDSTRING), static_cast(0), reinterpret_cast(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(selection), static_cast(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 ) { memset( g_DemoTypeFile, 0, sizeof( g_DemoTypeFile ) ); @@ -2176,7 +2272,7 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message, break; case WM_CTLCOLORSTATIC: - if( reinterpret_cast(lParam) == GetDlgItem( hDlg, IDC_TITLE ) || + if( reinterpret_cast(lParam) == GetDlgItem( hDlg, IDC_TITLE ) || reinterpret_cast(lParam) == GetDlgItem(hDlg, IDC_DRAWING) || reinterpret_cast(lParam) == GetDlgItem(hDlg, IDC_ZOOM) || reinterpret_cast(lParam) == GetDlgItem(hDlg, IDC_BREAK) || @@ -2249,7 +2345,17 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message, text[2] = 0; newTimeout = _tstoi( text ); - g_RecordFrameRate = g_FramerateOptions[SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FRAME_RATE), static_cast(CB_GETCURSEL), static_cast(0), static_cast(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(CB_GETCURSEL), static_cast(0), static_cast(0))]; + } + + g_RecordingFormat = static_cast(SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FORMAT), static_cast(CB_GETCURSEL), static_cast(0), static_cast(0))); g_RecordScaling = static_cast(SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_SCALING), static_cast(CB_GETCURSEL), static_cast(0), static_cast(0)) * 10 + 10); // Get the selected microphone @@ -2263,7 +2369,7 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message, UnregisterAllHotkeys(GetParent( hDlg )); break; - } else if(newLiveZoomToggleKey && + } else if(newLiveZoomToggleKey && (!RegisterHotKey( GetParent( hDlg ), LIVE_HOTKEY, newLiveZoomToggleMod, newLiveZoomToggleKey & 0xFF ) || !RegisterHotKey(GetParent(hDlg), LIVE_DRAW_HOTKEY, (newLiveZoomToggleMod ^ MOD_SHIFT), newLiveZoomToggleKey & 0xFF))) { @@ -2286,7 +2392,7 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message, UnregisterAllHotkeys(GetParent( hDlg )); break; - } else if( newDemoTypeToggleKey && + } else if( newDemoTypeToggleKey && (!RegisterHotKey( GetParent( hDlg ), DEMOTYPE_HOTKEY, newDemoTypeToggleMod, newDemoTypeToggleKey & 0xFF ) || !RegisterHotKey(GetParent(hDlg), DEMOTYPE_RESET_HOTKEY, (newDemoTypeToggleMod ^ MOD_SHIFT), newDemoTypeToggleKey & 0xFF))) { @@ -2296,7 +2402,7 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message, break; } - else if (newSnipToggleKey && + else if (newSnipToggleKey && (!RegisterHotKey(GetParent(hDlg), SNIP_HOTKEY, newSnipToggleMod, newSnipToggleKey & 0xFF) || !RegisterHotKey(GetParent(hDlg), SNIP_SAVE_HOTKEY, (newSnipToggleMod ^ MOD_SHIFT), newSnipToggleKey & 0xFF))) { @@ -2305,8 +2411,8 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message, UnregisterAllHotkeys(GetParent(hDlg)); break; - } - else if( newRecordToggleKey && + } + else if( newRecordToggleKey && (!RegisterHotKey(GetParent(hDlg), RECORD_HOTKEY, newRecordToggleMod | MOD_NOREPEAT, newRecordToggleKey & 0xFF) || !RegisterHotKey(GetParent(hDlg), RECORD_CROP_HOTKEY, (newRecordToggleMod ^ MOD_SHIFT) | MOD_NOREPEAT, newRecordToggleKey & 0xFF) || !RegisterHotKey(GetParent(hDlg), RECORD_WINDOW_HOTKEY, (newRecordToggleMod ^ MOD_ALT) | MOD_NOREPEAT, newRecordToggleKey & 0xFF))) { @@ -2317,7 +2423,7 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message, break; } else { - + g_BreakTimeout = newTimeout; g_ToggleKey = newToggleKey; g_LiveZoomToggleKey = newLiveZoomToggleKey; @@ -2337,7 +2443,7 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message, hWndOptions = NULL; EndDialog( hDlg, 0 ); - return TRUE; + return TRUE; } break; } @@ -2348,8 +2454,8 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message, EndDialog( hDlg, 0 ); return TRUE; } - break; - + break; + case WM_CLOSE: hWndOptions = NULL; RegisterAllHotkeys(GetParent(hDlg)); @@ -2388,7 +2494,7 @@ void DeleteDrawUndoList( P_DRAW_UNDO *DrawUndoList ) // PopDrawUndo // //---------------------------------------------------------------------------- -BOOLEAN PopDrawUndo( HDC hDc, P_DRAW_UNDO *DrawUndoList, +BOOLEAN PopDrawUndo( HDC hDc, P_DRAW_UNDO *DrawUndoList, int width, int height ) { P_DRAW_UNDO nextUndo; @@ -2396,7 +2502,7 @@ BOOLEAN PopDrawUndo( HDC hDc, P_DRAW_UNDO *DrawUndoList, nextUndo = *DrawUndoList; if( nextUndo ) { - BitBlt( hDc, 0, 0, width, height, + BitBlt( hDc, 0, 0, width, height, nextUndo->hDc, 0, 0, SRCCOPY|CAPTUREBLT ); *DrawUndoList = nextUndo->Next; DeleteObject( nextUndo->hBitmap ); @@ -2444,7 +2550,7 @@ void DeleteOldestUndo( P_DRAW_UNDO *DrawUndoList ) //---------------------------------------------------------------------------- // // GetOldestUndo -// +// //---------------------------------------------------------------------------- P_DRAW_UNDO GetOldestUndo(P_DRAW_UNDO DrawUndoList) { @@ -2509,7 +2615,7 @@ void PushDrawUndo( HDC hDc, P_DRAW_UNDO *DrawUndoList, int width, int height ) newUndo->Next = *DrawUndoList; *DrawUndoList = newUndo; } - } + } } //---------------------------------------------------------------------------- @@ -2562,8 +2668,8 @@ void ClearTypingCursor( HDC hdcScreenCompat, HDC hdcScreenCursorCompat, RECT rc, } else { - BitBlt(hdcScreenCompat, rc.left, rc.top, rc.right - rc.left, - rc.bottom - rc.top, hdcScreenCursorCompat,0, 0, SRCCOPY|CAPTUREBLT ); + BitBlt(hdcScreenCompat, rc.left, rc.top, rc.right - rc.left, + rc.bottom - rc.top, hdcScreenCursorCompat,0, 0, SRCCOPY|CAPTUREBLT ); } } @@ -2618,7 +2724,7 @@ RECT BoundMouse( float zoomLevel, MONITORINFO *monInfo, int width, int height, int x, y; GetZoomedTopLeftCoordinates( zoomLevel, cursorPos, &x, width, &y, height ); - rc.left = monInfo->rcMonitor.left + x; + rc.left = monInfo->rcMonitor.left + x; rc.right = rc.left + static_cast(width/zoomLevel); rc.top = monInfo->rcMonitor.top + y; rc.bottom = rc.top + static_cast(height/zoomLevel); @@ -2629,7 +2735,7 @@ RECT BoundMouse( float zoomLevel, MONITORINFO *monInfo, int width, int height, rc.left, rc.top, rc.right, rc.bottom); OutputDebug( L"mon.left: %d mon.top: %d mon.right: %d mon.bottom: %d\n", monInfo->rcMonitor.left, monInfo->rcMonitor.top, monInfo->rcMonitor.right, monInfo->rcMonitor.bottom); - + ClipCursor( &rc ); return rc; } @@ -2657,7 +2763,7 @@ void DrawArrow( HDC hdc, int x1, int y1, int x2, int y2, double length, double w // get midpoint of base int xMid = x2 - static_cast(length*dx+0.5); int yMid = y2 - static_cast(length*dy+0.5); - + // get left wing int xLeft = xMid - static_cast(dy*width+0.5); int yLeft = yMid + static_cast(dx*width+0.5); @@ -2763,7 +2869,7 @@ VOID DrawShape( DWORD Shape, HDC hDc, RECT *Rect, bool UseGdiPlus = false ) case DRAW_RECTANGLE: if (UseGdiPlus) if(pBrush) - DrawHighlightedShape(DRAW_RECTANGLE, hDc, pBrush, NULL, + DrawHighlightedShape(DRAW_RECTANGLE, hDc, pBrush, NULL, static_cast(Rect->left - 1), static_cast(Rect->top - 1), static_cast(Rect->right), static_cast(Rect->bottom)); else if (isBlur) @@ -2840,7 +2946,7 @@ VOID SendPenMessage(HWND hWnd, UINT Message, LPARAM lParam) if(GetKeyState(VK_LCONTROL) < 0 ) { wParam |= MK_CONTROL; - } + } if( GetKeyState( VK_LSHIFT) < 0 || GetKeyState( VK_RSHIFT) < 0 ) { wParam |= MK_SHIFT; @@ -2853,10 +2959,10 @@ VOID SendPenMessage(HWND hWnd, UINT Message, LPARAM lParam) //---------------------------------------------------------------------------- // // ScalePenPosition -// +// // Maps pen input to mouse input coordinates based on zoom level. Returns // 0 if pen is active but we didn't send this message to ourselves (pen -// signature will be missing). +// signature will be missing). // //---------------------------------------------------------------------------- LPARAM ScalePenPosition( float zoomLevel, MONITORINFO *monInfo, RECT boundRc, @@ -2867,7 +2973,7 @@ LPARAM ScalePenPosition( float zoomLevel, MONITORINFO *monInfo, RECT boundRc, LPARAM extraInfo; extraInfo = GetMessageExtraInfo(); - if( g_PenDown ) { + if( g_PenDown ) { // ignore messages we didn't tag as pen if (extraInfo == MI_WP_SIGNATURE) { @@ -2890,7 +2996,7 @@ LPARAM ScalePenPosition( float zoomLevel, MONITORINFO *monInfo, RECT boundRc, OutputDebug(L"Ignore pen message we didn't send\n"); lParam = 0; } - + } else { if( !GetClipCursor( &rc )) { @@ -2900,7 +3006,7 @@ LPARAM ScalePenPosition( float zoomLevel, MONITORINFO *monInfo, RECT boundRc, OutputDebug( L"Mouse message\n"); } return lParam; -} +} //---------------------------------------------------------------------------- @@ -2927,7 +3033,7 @@ BOOLEAN DrawHighlightedCursor( float ZoomLevel, int Width, int Height ) // InvalidateCursorMoveArea // //---------------------------------------------------------------------------- -void InvalidateCursorMoveArea( HWND hWnd, float zoomLevel, int width, int height, +void InvalidateCursorMoveArea( HWND hWnd, float zoomLevel, int width, int height, POINT currentPt, POINT prevPt, POINT cursorPos ) { int x, y; @@ -2935,7 +3041,7 @@ void InvalidateCursorMoveArea( HWND hWnd, float zoomLevel, int width, int height int invWidth = g_PenWidth + CURSOR_SAVE_MARGIN; if( DrawHighlightedCursor( zoomLevel, width, height ) ) { - + invWidth = g_PenWidth * 3 + 1; } GetZoomedTopLeftCoordinates( zoomLevel, &cursorPos, &x, width, &y, height ); @@ -3058,7 +3164,7 @@ void DrawCursor( HDC hDcTarget, POINT pt, float ZoomLevel, int Width, int Height // //---------------------------------------------------------------------------- void ResizePen( HWND hWnd, HDC hdcScreenCompat, HDC hdcScreenCursorCompat, POINT prevPt, - BOOLEAN g_Tracing, BOOLEAN *g_Drawing, float g_LiveZoomLevel, + BOOLEAN g_Tracing, BOOLEAN *g_Drawing, float g_LiveZoomLevel, BOOLEAN isUser, int newWidth ) { if( !g_Tracing ) { @@ -3117,7 +3223,7 @@ bool IsPenInverted( WPARAM wParam ) //---------------------------------------------------------------------------- // // CaptureScreenshotAsync -// +// // Captures the specified screen using the capture APIs // //---------------------------------------------------------------------------- @@ -3127,7 +3233,7 @@ std::future> CaptureScreenshotAsync(winrt::IDire winrt::com_ptr d3dContext; d3dDevice->GetImmediateContext(d3dContext.put()); - // Creating our frame pool with CreateFreeThreaded means that we + // Creating our frame pool with CreateFreeThreaded means that we // will be called back from the frame pool's internal worker thread // instead of the thread we are currently on. It also disables the // DispatcherQueue requirement. @@ -3165,7 +3271,7 @@ std::future> CaptureScreenshotAsync(winrt::IDire //---------------------------------------------------------------------------- // // CaptureScreenshot -// +// // Captures the specified screen using the capture APIs // //---------------------------------------------------------------------------- @@ -3196,9 +3302,9 @@ winrt::com_ptrCaptureScreenshot(winrt::DirectXPixelFormat const //---------------------------------------------------------------------------- // // CopyD3DTexture -// +// //---------------------------------------------------------------------------- -inline auto CopyD3DTexture(winrt::com_ptr const& device, +inline auto CopyD3DTexture(winrt::com_ptr const& device, winrt::com_ptr const& texture, bool asStagingTexture) { winrt::com_ptr context; @@ -3224,9 +3330,9 @@ inline auto CopyD3DTexture(winrt::com_ptr const& device, //---------------------------------------------------------------------------- // // PrepareStagingTexture -// +// //---------------------------------------------------------------------------- -inline auto PrepareStagingTexture(winrt::com_ptr const& device, +inline auto PrepareStagingTexture(winrt::com_ptr const& device, winrt::com_ptr const& texture) { // If our texture is already set up for staging, then use it. @@ -3244,7 +3350,7 @@ inline auto PrepareStagingTexture(winrt::com_ptr const& device, //---------------------------------------------------------------------------- // // GetBytesPerPixel -// +// //---------------------------------------------------------------------------- inline size_t GetBytesPerPixel(DXGI_FORMAT pixelFormat) @@ -3342,7 +3448,7 @@ GetBytesPerPixel(DXGI_FORMAT pixelFormat) //---------------------------------------------------------------------------- // // CopyBytesFromTexture -// +// //---------------------------------------------------------------------------- inline auto CopyBytesFromTexture(winrt::com_ptr const& texture, uint32_t subresource = 0) { @@ -3395,6 +3501,12 @@ void StopRecording() g_RecordingSession = nullptr; } + if ( g_GifRecordingSession != nullptr ) { + + g_GifRecordingSession->Close(); + g_GifRecordingSession = nullptr; + } + g_RecordToggle = FALSE; #if WINDOWS_CURSOR_RECORDING_WORKAROUND @@ -3442,7 +3554,7 @@ auto GetUniqueRecordingFilename() //---------------------------------------------------------------------------- // // StartRecordingAsync -// +// // Starts the screen recording. // //---------------------------------------------------------------------------- @@ -3451,7 +3563,10 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR auto tempFolderPath = std::filesystem::temp_directory_path().wstring(); auto tempFolder = co_await winrt::StorageFolder::GetFolderFromPathAsync( tempFolderPath ); 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 auto d3dDevice = util::CreateD3D11Device(); @@ -3460,7 +3575,7 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR // Get the active MONITOR capture device HMONITOR hMon = NULL; - POINT cursorPos = { 0, 0 }; + POINT cursorPos = { 0, 0 }; if( pMonitorFromPoint ) { GetCursorPos( &cursorPos ); @@ -3468,27 +3583,46 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR } winrt::Windows::Graphics::Capture::GraphicsCaptureItem item{ nullptr }; - if( hWndRecord ) + if( hWndRecord ) item = util::CreateCaptureItemForWindow( hWndRecord ); else item = util::CreateCaptureItemForMonitor( hMon ); 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 ) - g_RecordingSession->EnableCursorCapture( false ); + // Create the appropriate recording session based on format + 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 - if( g_RecordingSession == nullptr ) { + co_await g_GifRecordingSession->StartAsync(); + } + 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; @@ -3504,11 +3638,24 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR wil::com_ptr videosItem; if( SUCCEEDED ( SHGetKnownFolderItem( FOLDERID_Videos, KF_FLAG_DEFAULT, nullptr, IID_IShellItem, (void**) videosItem.put() ) ) ) saveDialog->SetDefaultFolder( videosItem.get() ); - saveDialog->SetDefaultExtension( L".mp4" ); - COMDLG_FILTERSPEC fileTypes[] = { - { L"MP4 Video", L"*.mp4" } - }; - saveDialog->SetFileTypes( _countof( fileTypes ), fileTypes ); + + // Set file type based on the recording format + if (g_RecordingFormat == RecordingFormat::GIF) + { + 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) { @@ -3516,8 +3663,12 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR wil::unique_cotaskmem_string folderPath; if (SUCCEEDED(saveDialog->GetFolder(shellItem.put())) && SUCCEEDED(shellItem->GetDisplayName(SIGDN_FILESYSPATH, folderPath.put()))) 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(); saveDialog->SetFileName( suggestedName.c_str() ); @@ -3566,6 +3717,7 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR } co_await file.DeleteAsync(); g_RecordingSession = nullptr; + g_GifRecordingSession = nullptr; } } catch( const winrt::hresult_error& error ) { @@ -3681,9 +3833,9 @@ void ShowMainWindow(HWND hWnd, const MONITORINFO& monInfo, int width, int height // //---------------------------------------------------------------------------- LRESULT APIENTRY MainWndProc( - HWND hWnd, + HWND hWnd, UINT message, - WPARAM wParam, + WPARAM wParam, LPARAM lParam) { static int width, height; @@ -3742,7 +3894,7 @@ LRESULT APIENTRY MainWndProc( #endif bool isCaptureSupported = false; RECT rc, rc1; - PAINTSTRUCT ps; + PAINTSTRUCT ps; TCHAR timerText[16]; TCHAR negativeTimerText[16]; BOOLEAN penInverted; @@ -3784,15 +3936,22 @@ LRESULT APIENTRY MainWndProc( case WM_CREATE: // get default font - GetObject( GetStockObject(DEFAULT_GUI_FONT), sizeof g_LogFont, &g_LogFont ); + GetObject( GetStockObject(DEFAULT_GUI_FONT), sizeof g_LogFont, &g_LogFont ); g_LogFont.lfWeight = FW_NORMAL; hDc = CreateCompatibleDC( NULL ); g_LogFont.lfHeight = -MulDiv(8, GetDeviceCaps(hDc, LOGPIXELSY), 72); DeleteDC( hDc ); reg.ReadRegSettings( RegSettings ); - - // to support migrating from + + // 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 if ((g_PenColor >> 24) == 0) { g_PenColor |= 0xFF << 24; } @@ -3822,7 +3981,7 @@ LRESULT APIENTRY MainWndProc( APPNAME, MB_ICONERROR ); showOptions = TRUE; - } else if( g_LiveZoomToggleKey && + } else if( g_LiveZoomToggleKey && (!RegisterHotKey( hWnd, LIVE_HOTKEY, g_LiveZoomToggleMod, g_LiveZoomToggleKey & 0xFF) || !RegisterHotKey(hWnd, LIVE_DRAW_HOTKEY, (g_LiveZoomToggleMod ^ MOD_SHIFT), g_LiveZoomToggleKey & 0xFF))) { @@ -3844,7 +4003,7 @@ LRESULT APIENTRY MainWndProc( showOptions = TRUE; } - else if( g_DemoTypeToggleKey && + else if( g_DemoTypeToggleKey && (!RegisterHotKey( hWnd, DEMOTYPE_HOTKEY, g_DemoTypeToggleMod, g_DemoTypeToggleKey & 0xFF ) || !RegisterHotKey(hWnd, DEMOTYPE_RESET_HOTKEY, (g_DemoTypeToggleMod ^ MOD_SHIFT), g_DemoTypeToggleKey & 0xFF))) { @@ -3853,7 +4012,7 @@ LRESULT APIENTRY MainWndProc( showOptions = TRUE; } - else if (g_SnipToggleKey && + else if (g_SnipToggleKey && (!RegisterHotKey(hWnd, SNIP_HOTKEY, g_SnipToggleMod, g_SnipToggleKey & 0xFF) || !RegisterHotKey(hWnd, SNIP_SAVE_HOTKEY, (g_SnipToggleMod ^ MOD_SHIFT), g_SnipToggleKey & 0xFF))) { @@ -3862,7 +4021,7 @@ LRESULT APIENTRY MainWndProc( showOptions = TRUE; } - else if (g_RecordToggleKey && + else if (g_RecordToggleKey && (!RegisterHotKey(hWnd, RECORD_HOTKEY, g_RecordToggleMod | 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))) { @@ -3985,7 +4144,7 @@ LRESULT APIENTRY MainWndProc( // Highlight is not supported in LiveDraw g_PenColor |= 0xFF << 24; } - } + } break; case SNIP_SAVE_HOTKEY: @@ -4156,7 +4315,7 @@ LRESULT APIENTRY MainWndProc( if( g_hWndLiveZoom == NULL ) { OutputDebug(L"Create LIVEZOOM\n"); g_hWndLiveZoom = CreateWindowEx( WS_EX_TOOLWINDOW | WS_EX_LAYERED | WS_EX_TRANSPARENT, - L"MagnifierClass", L"ZoomIt Live Zoom", + L"MagnifierClass", L"ZoomIt Live Zoom", WS_POPUP | WS_CLIPSIBLINGS, 0, 0, 0, 0, NULL, NULL, g_hInstance, static_cast(GetForegroundWindow()) ); pSetLayeredWindowAttributes( hWnd, 0, 0, LWA_ALPHA ); @@ -4179,10 +4338,10 @@ LRESULT APIENTRY MainWndProc( g_LiveZoomLevel = g_ZoomLevels[g_SliderZoomLevel]; #endif // Unzoom - SendMessage( g_hWndLiveZoom, WM_KEYDOWN, VK_ESCAPE, 0 ); + SendMessage( g_hWndLiveZoom, WM_KEYDOWN, VK_ESCAPE, 0 ); } else { - + OutputDebug(L"Show liveZoom\n"); ShowWindow( g_hWndLiveZoom, SW_SHOW ); } @@ -4206,6 +4365,8 @@ LRESULT APIENTRY MainWndProc( case RECORD_HOTKEY: case RECORD_CROP_HOTKEY: case RECORD_WINDOW_HOTKEY: + case RECORD_GIF_HOTKEY: + case RECORD_GIF_WINDOW_HOTKEY: // // Recording @@ -4221,7 +4382,7 @@ LRESULT APIENTRY MainWndProc( if( g_RecordCropping == TRUE ) { break; - } + } // Start screen recording try @@ -4243,7 +4404,7 @@ LRESULT APIENTRY MainWndProc( { // Already recording break; - } + } g_RecordCropping = TRUE; @@ -4334,8 +4495,8 @@ LRESULT APIENTRY MainWndProc( { cropRc = {}; - // if we're recording a window, get the window - if (wParam == RECORD_WINDOW_HOTKEY) + // if we're recording a window, get the window + if (wParam == RECORD_WINDOW_HOTKEY || wParam == RECORD_GIF_WINDOW_HOTKEY) { GetCursorPos(&cursorPos); hWndRecord = WindowFromPoint(cursorPos); @@ -4353,6 +4514,7 @@ LRESULT APIENTRY MainWndProc( if( g_RecordToggle == FALSE ) { g_RecordToggle = TRUE; + #ifdef __ZOOMIT_POWERTOYS__ if( g_StartedByPowerToys ) { @@ -4379,7 +4541,7 @@ LRESULT APIENTRY MainWndProc( break; } - + OutputDebug( L"ZOOM HOTKEY: %d\n", lParam); if( g_TimerActive ) { @@ -4444,9 +4606,9 @@ LRESULT APIENTRY MainWndProc( // Get screen DCs hdcScreen = CreateDC(L"DISPLAY", static_cast(NULL), static_cast(NULL), static_cast(NULL)); - hdcScreenCompat = CreateCompatibleDC(hdcScreen); - hdcScreenSaveCompat = CreateCompatibleDC(hdcScreen); - hdcScreenCursorCompat = CreateCompatibleDC(hdcScreen); + hdcScreenCompat = CreateCompatibleDC(hdcScreen); + hdcScreenSaveCompat = CreateCompatibleDC(hdcScreen); + hdcScreenCursorCompat = CreateCompatibleDC(hdcScreen); // Determine what monitor we're on GetCursorPos(&cursorPos); @@ -4461,13 +4623,13 @@ LRESULT APIENTRY MainWndProc( bmp.bmPlanes = static_cast(GetDeviceCaps(hdcScreen, PLANES)); bmp.bmWidth = width; bmp.bmHeight = height; - bmp.bmWidthBytes = ((bmp.bmWidth + 15) &~15)/8; - hbmpCompat = CreateBitmap(bmp.bmWidth, bmp.bmHeight, + bmp.bmWidthBytes = ((bmp.bmWidth + 15) &~15)/8; + hbmpCompat = CreateBitmap(bmp.bmWidth, bmp.bmHeight, bmp.bmPlanes, bmp.bmBitsPixel, static_cast(NULL)); - SelectObject(hdcScreenCompat, hbmpCompat); + SelectObject(hdcScreenCompat, hbmpCompat); // Create saved bitmap - hbmpDrawingCompat = CreateBitmap(bmp.bmWidth, bmp.bmHeight, + hbmpDrawingCompat = CreateBitmap(bmp.bmWidth, bmp.bmHeight, bmp.bmPlanes, bmp.bmBitsPixel, static_cast(NULL)); SelectObject(hdcScreenSaveCompat, hbmpDrawingCompat); @@ -4542,7 +4704,7 @@ LRESULT APIENTRY MainWndProc( g_TypeMode = TypeModeOff; g_HaveDrawn = FALSE; EnableDisableStickyKeys( TRUE ); - + // Go full screen g_ActiveWindow = GetForegroundWindow(); OutputDebug( L"active window: %x\n", PtrToLong(g_ActiveWindow) ); @@ -4552,7 +4714,7 @@ LRESULT APIENTRY MainWndProc( OutputDebug(L"Calling ShowMainWindow\n"); ShowMainWindow(hWnd, monInfo, width, height); } - + // Start telescoping zoom. Lparam is non-zero if this // was a real hotkey and not the message we send ourself to enter // unzoomed drawing mode. @@ -4570,7 +4732,7 @@ LRESULT APIENTRY MainWndProc( OutputDebug(L"Enter liveZoom draw\n"); g_LiveZoomSourceRect = *reinterpret_cast(SendMessage( g_hWndLiveZoom, WM_USER_GET_SOURCE_RECT, 0, 0 )); g_LiveZoomLevel = *reinterpret_cast(SendMessage(g_hWndLiveZoom, WM_USER_GET_ZOOM_LEVEL, 0, 0)); - + // Set live zoom level to 1 in preparation of us being full screen static zoomLevel = 1.0; zoomTelescopeTarget = 1.0; @@ -4613,8 +4775,8 @@ LRESULT APIENTRY MainWndProc( zoomTelescopeStep = ZOOM_LEVEL_STEP_IN; zoomTelescopeTarget = g_ZoomLevels[g_SliderZoomLevel]; - if( g_AnimateZoom ) - zoomLevel = static_cast(1.0) * zoomTelescopeStep; + if( g_AnimateZoom ) + zoomLevel = static_cast(1.0) * zoomTelescopeStep; else zoomLevel = zoomTelescopeTarget; SetTimer( hWnd, 1, ZOOM_LEVEL_STEP_TIME, NULL ); @@ -4629,7 +4791,7 @@ LRESULT APIENTRY MainWndProc( if( lParam != SHALLOW_DESTROY && !g_ZoomOnLiveZoom && g_AnimateZoom && g_TelescopeZoomOut && zoomTelescopeTarget != 1 ) { - // Start telescoping zoom. + // Start telescoping zoom. zoomTelescopeStep = ZOOM_LEVEL_STEP_OUT; zoomTelescopeTarget = 1.0; SetTimer( hWnd, 2, ZOOM_LEVEL_STEP_TIME, NULL ); @@ -4682,7 +4844,7 @@ LRESULT APIENTRY MainWndProc( } break; - case WM_POINTERDOWN: + case WM_POINTERDOWN: OutputDebug(L"WM_POINTERDOWN\n"); penInverted = IsPenInverted(wParam); if (!penInverted) { @@ -4719,15 +4881,15 @@ LRESULT APIENTRY MainWndProc( // // Zoom or modify break timer // - if( GET_WHEEL_DELTA_WPARAM(wParam) < 0 ) + if( GET_WHEEL_DELTA_WPARAM(wParam) < 0 ) wParam -= (WHEEL_DELTA-1) << 16; - else + else wParam += (WHEEL_DELTA-1) << 16; delta = GET_WHEEL_DELTA_WPARAM(wParam)/WHEEL_DELTA; - OutputDebug( L"mousewheel: wParam: %d delta: %d\n", + OutputDebug( L"mousewheel: wParam: %d delta: %d\n", GET_WHEEL_DELTA_WPARAM(wParam), delta ); if( g_Zoomed ) { - + if( g_TypeMode == TypeModeOff ) { if( g_Drawing && (LOWORD( wParam ) & MK_CONTROL) ) { @@ -4746,7 +4908,7 @@ LRESULT APIENTRY MainWndProc( while( delta-- ) { if( zoomIn ) { - + if( zoomTelescopeTarget < ZOOM_LEVEL_MAX ) { if( zoomTelescopeTarget < 2 ) { @@ -4754,17 +4916,17 @@ LRESULT APIENTRY MainWndProc( zoomTelescopeTarget = 2; } else { - + // Start telescoping zoom - zoomTelescopeTarget = zoomTelescopeTarget * 2; + zoomTelescopeTarget = zoomTelescopeTarget * 2; } - zoomTelescopeStep = ZOOM_LEVEL_STEP_IN; - if( g_AnimateZoom ) - zoomLevel *= zoomTelescopeStep; + zoomTelescopeStep = ZOOM_LEVEL_STEP_IN; + if( g_AnimateZoom ) + zoomLevel *= zoomTelescopeStep; else zoomLevel = zoomTelescopeTarget; - if( zoomLevel > zoomTelescopeTarget ) + if( zoomLevel > zoomTelescopeTarget ) zoomLevel = zoomTelescopeTarget; else SetTimer( hWnd, 1, ZOOM_LEVEL_STEP_TIME, NULL ); @@ -4775,17 +4937,17 @@ LRESULT APIENTRY MainWndProc( // Let them more gradually zoom out from 2x to 1x if( zoomTelescopeTarget <= 2 ) { - zoomTelescopeTarget *= .75; - if( zoomTelescopeTarget < ZOOM_LEVEL_MIN ) + zoomTelescopeTarget *= .75; + if( zoomTelescopeTarget < ZOOM_LEVEL_MIN ) zoomTelescopeTarget = ZOOM_LEVEL_MIN; } else { - zoomTelescopeTarget = zoomTelescopeTarget/2; + zoomTelescopeTarget = zoomTelescopeTarget/2; } - zoomTelescopeStep = ZOOM_LEVEL_STEP_OUT; - if( g_AnimateZoom ) - zoomLevel *= zoomTelescopeStep; + zoomTelescopeStep = ZOOM_LEVEL_STEP_OUT; + if( g_AnimateZoom ) + zoomLevel *= zoomTelescopeStep; else zoomLevel = zoomTelescopeTarget; @@ -4809,11 +4971,11 @@ LRESULT APIENTRY MainWndProc( RestoreCursorArea( hdcScreenCompat, hdcScreenCursorCompat, prevPt ); } - //SetCursorPos( monInfo.rcMonitor.left + cursorPos.x, + //SetCursorPos( monInfo.rcMonitor.left + cursorPos.x, // monInfo.rcMonitor.top + cursorPos.y ); } InvalidateRect( hWnd, NULL, FALSE ); - } + } } } else { @@ -4827,7 +4989,7 @@ LRESULT APIENTRY MainWndProc( // Set lParam to 0 as part of message to keyup hander DeleteObject(hTypingFont); g_LogFont.lfHeight = max((int)(height / zoomLevel) / g_FontScale, 12); - if (g_LogFont.lfHeight < 20) + if (g_LogFont.lfHeight < 20) g_LogFont.lfQuality = NONANTIALIASED_QUALITY; else g_LogFont.lfQuality = ANTIALIASED_QUALITY; @@ -4934,7 +5096,7 @@ LRESULT APIENTRY MainWndProc( OutputDebug(L"Entering typing mode and resetting cursor position\n"); SendMessage( hWnd, WM_LBUTTONDOWN, 0, MAKELPARAM( cursorPos.x, cursorPos.y)); - } + } // Do they want to right-justify text? OutputDebug(L"Keyup Shift: %x\n", GetAsyncKeyState(VK_SHIFT)); @@ -4964,13 +5126,13 @@ LRESULT APIENTRY MainWndProc( g_LogFont.lfQuality = ANTIALIASED_QUALITY; hTypingFont = CreateFontIndirect( &g_LogFont ); SelectObject( hdcScreenCompat, hTypingFont ); - + // If lparam == 0 that means that we sent the message as part of a font resize if( g_Drawing && lParam != 0) { RestoreCursorArea( hdcScreenCompat, hdcScreenCursorCompat, prevPt ); PushDrawUndo( hdcScreenCompat, &drawUndoList, width, height ); - + } else if( !g_Drawing ) { textPt = cursorPos; @@ -4985,7 +5147,7 @@ LRESULT APIENTRY MainWndProc( case WM_KEYDOWN: if( (g_TypeMode != TypeModeOff) && g_HaveTyped && static_cast(wParam) != VK_UP && static_cast(wParam) != VK_DOWN && - (isprint( static_cast(wParam)) || + (isprint( static_cast(wParam)) || wParam == VK_RETURN || wParam == VK_DELETE || wParam == VK_BACK )) { if( wParam == VK_RETURN ) { @@ -5077,10 +5239,10 @@ LRESULT APIENTRY MainWndProc( } DrawTypingCursor( hWnd, &textPt, hdcScreenCompat, hdcScreenCursorCompat, &cursorRc ); } - } + } break; } - switch (wParam) { + switch (wParam) { case 'R': case 'B': case 'Y': @@ -5089,7 +5251,7 @@ LRESULT APIENTRY MainWndProc( case 'X': case 'P': if( (g_Zoomed || g_TimerActive) && (g_TypeMode == TypeModeOff)) { - + PDWORD penColor; if( g_TimerActive ) penColor = &g_BreakPenColor; @@ -5141,12 +5303,12 @@ LRESULT APIENTRY MainWndProc( SelectObject( hdcScreenCompat, hDrawingPen ); if( g_Drawing ) { - SendMessage( hWnd, WM_MOUSEMOVE, 0, MAKELPARAM( prevPt.x, prevPt.y )); - + SendMessage( hWnd, WM_MOUSEMOVE, 0, MAKELPARAM( prevPt.x, prevPt.y )); + } else if( g_TimerActive ) { - - InvalidateRect( hWnd, NULL, FALSE ); - + + InvalidateRect( hWnd, NULL, FALSE ); + } else if( g_TypeMode != TypeModeOff ) { ClearTypingCursor( hdcScreenCompat, hdcScreenCursorCompat, cursorRc, g_BlankedScreen ); @@ -5160,11 +5322,11 @@ LRESULT APIENTRY MainWndProc( if( (GetKeyState( VK_CONTROL ) & 0x8000 ) && g_HaveDrawn && !g_Tracing ) { if( PopDrawUndo( hdcScreenCompat, &drawUndoList, width, height )) { - + if( g_Drawing ) { SaveCursorArea( hdcScreenCursorCompat, hdcScreenCompat, prevPt ); - SendMessage( hWnd, WM_MOUSEMOVE, 0, MAKELPARAM( prevPt.x, prevPt.y )); + SendMessage( hWnd, WM_MOUSEMOVE, 0, MAKELPARAM( prevPt.x, prevPt.y )); } else { @@ -5180,7 +5342,7 @@ LRESULT APIENTRY MainWndProc( SetCursorPos( boundRc.left + (boundRc.right - boundRc.left)/2, boundRc.top + (boundRc.bottom - boundRc.top)/2 ); - SendMessage( hWnd, WM_MOUSEMOVE, 0, + SendMessage( hWnd, WM_MOUSEMOVE, 0, MAKELPARAM( (boundRc.right - boundRc.left)/2, (boundRc.bottom - boundRc.top)/2 )); } @@ -5215,7 +5377,7 @@ LRESULT APIENTRY MainWndProc( // Save area that's going to be occupied by new cursor position SaveCursorArea( hdcScreenCursorCompat, hdcScreenCompat, prevPt ); - SendMessage( hWnd, WM_MOUSEMOVE, 0, MAKELPARAM( prevPt.x, prevPt.y )); + SendMessage( hWnd, WM_MOUSEMOVE, 0, MAKELPARAM( prevPt.x, prevPt.y )); } break; @@ -5243,18 +5405,18 @@ LRESULT APIENTRY MainWndProc( } InvalidateRect( hWnd, NULL, FALSE ); g_BlankedScreen = FALSE; - } + } break; case VK_UP: - SendMessage( hWnd, WM_MOUSEWHEEL, - MAKEWPARAM( GetAsyncKeyState( VK_LCONTROL ) != 0 || GetAsyncKeyState( VK_RCONTROL ) != 0 ? + SendMessage( hWnd, WM_MOUSEWHEEL, + MAKEWPARAM( GetAsyncKeyState( VK_LCONTROL ) != 0 || GetAsyncKeyState( VK_RCONTROL ) != 0 ? MK_CONTROL: 0, WHEEL_DELTA), 0 ); return TRUE; case VK_DOWN: - SendMessage( hWnd, WM_MOUSEWHEEL, - MAKEWPARAM( GetAsyncKeyState( VK_LCONTROL ) != 0 || GetAsyncKeyState( VK_RCONTROL ) != 0 ? + SendMessage( hWnd, WM_MOUSEWHEEL, + MAKEWPARAM( GetAsyncKeyState( VK_LCONTROL ) != 0 || GetAsyncKeyState( VK_RCONTROL ) != 0 ? MK_CONTROL: 0, -WHEEL_DELTA), 0 ); return TRUE; @@ -5273,13 +5435,13 @@ LRESULT APIENTRY MainWndProc( InvalidateRect( hWnd, NULL, TRUE ); } break; - - case VK_ESCAPE: + + case VK_ESCAPE: if( g_TypeMode != TypeModeOff) { // Turn off SendMessage( hWnd, WM_USER_TYPING_OFF, 0, 0 ); - + } else { forcePenResize = TRUE; @@ -5318,7 +5480,7 @@ LRESULT APIENTRY MainWndProc( POINT currentPt; // Are we in pen mode on a tablet? - lParam = ScalePenPosition( zoomLevel, &monInfo, boundRc, message, lParam); + lParam = ScalePenPosition( zoomLevel, &monInfo, boundRc, message, lParam); currentPt.x = LOWORD(lParam); currentPt.y = HIWORD(lParam); @@ -5330,10 +5492,10 @@ LRESULT APIENTRY MainWndProc( } else if(g_DrawingShape) { - SetROP2(hdcScreenCompat, R2_NOTXORPEN); - - // If a previous target rectangle exists, erase - // it by drawing another rectangle on top. + SetROP2(hdcScreenCompat, R2_NOTXORPEN); + + // If a previous target rectangle exists, erase + // it by drawing another rectangle on top. if( g_rcRectangle.top != g_rcRectangle.bottom || g_rcRectangle.left != g_rcRectangle.right ) { @@ -5365,12 +5527,12 @@ LRESULT APIENTRY MainWndProc( } } - // Save the coordinates of the target rectangle. + // Save the coordinates of the target rectangle. // Avoid invalid rectangles by ensuring that the - // value of the left coordinate is greater than - // that of the right, and that the value of the - // bottom coordinate is greater than that of - // the top. + // value of the left coordinate is greater than + // that of the right, and that the value of the + // bottom coordinate is greater than that of + // the top. if( g_DrawingShape == DRAW_LINE || g_DrawingShape == DRAW_ARROW ) { @@ -5379,36 +5541,36 @@ LRESULT APIENTRY MainWndProc( } else { - if ((g_RectangleAnchor.x < currentPt.x) && + if ((g_RectangleAnchor.x < currentPt.x) && (g_RectangleAnchor.y > currentPt.y)) { - SetRect(&g_rcRectangle, g_RectangleAnchor.x, currentPt.y, - currentPt.x, g_RectangleAnchor.y); + SetRect(&g_rcRectangle, g_RectangleAnchor.x, currentPt.y, + currentPt.x, g_RectangleAnchor.y); - } else if ((g_RectangleAnchor.x > currentPt.x) && + } else if ((g_RectangleAnchor.x > currentPt.x) && (g_RectangleAnchor.y > currentPt.y )) { - SetRect(&g_rcRectangle, currentPt.x, - currentPt.y, g_RectangleAnchor.x,g_RectangleAnchor.y); + SetRect(&g_rcRectangle, currentPt.x, + currentPt.y, g_RectangleAnchor.x,g_RectangleAnchor.y); - } else if ((g_RectangleAnchor.x > currentPt.x) && + } else if ((g_RectangleAnchor.x > currentPt.x) && (g_RectangleAnchor.y < currentPt.y )) { - SetRect(&g_rcRectangle, currentPt.x, g_RectangleAnchor.y, - g_RectangleAnchor.x, currentPt.y ); + SetRect(&g_rcRectangle, currentPt.x, g_RectangleAnchor.y, + g_RectangleAnchor.x, currentPt.y ); } else { - SetRect(&g_rcRectangle, g_RectangleAnchor.x, g_RectangleAnchor.y, - currentPt.x, currentPt.y ); + SetRect(&g_rcRectangle, g_RectangleAnchor.x, g_RectangleAnchor.y, + currentPt.x, currentPt.y ); } } if (g_rcRectangle.left != g_rcRectangle.right || g_rcRectangle.top != g_rcRectangle.bottom) { - // Draw the new target rectangle. + // Draw the new target rectangle. DrawShape(g_DrawingShape, hdcScreenCompat, &g_rcRectangle, PEN_COLOR_HIGHLIGHT(g_PenColor)); - OutputDebug(L"SHAPE: (%d, %d) - (%d, %d)\n", g_rcRectangle.left, g_rcRectangle.top, + OutputDebug(L"SHAPE: (%d, %d) - (%d, %d)\n", g_rcRectangle.left, g_rcRectangle.top, g_rcRectangle.right, g_rcRectangle.bottom); } @@ -5446,7 +5608,7 @@ LRESULT APIENTRY MainWndProc( BYTE* pPixels = static_cast(lineData->Scan0); // Create a GDI bitmap that's the size of the lineBounds rectangle - Gdiplus::Bitmap *blurBitmap = CreateGdiplusBitmap( hdcScreenCompat, // oldestUndo->hDc, + Gdiplus::Bitmap *blurBitmap = CreateGdiplusBitmap( hdcScreenCompat, // oldestUndo->hDc, lineBounds.X, lineBounds.Y, lineBounds.Width, lineBounds.Height); // Blur it @@ -5466,8 +5628,8 @@ LRESULT APIENTRY MainWndProc( // Draw new cursor DrawCursor(hdcScreenCompat, currentPt, zoomLevel, width, height); - } - else if(PEN_COLOR_HIGHLIGHT(g_PenColor)) { + } + else if(PEN_COLOR_HIGHLIGHT(g_PenColor)) { OutputDebug(L"HIGHLIGHT\n"); @@ -5502,7 +5664,7 @@ LRESULT APIENTRY MainWndProc( HDC hdcDIBOrig; HBITMAP hDibOrigBitmap, hDibBitmap; P_DRAW_UNDO oldestUndo = GetOldestUndo(drawUndoList); - BYTE* pDestPixels2 = CreateBitmapMemoryDIB(hdcScreenCompat, oldestUndo->hDc, &lineBounds, + BYTE* pDestPixels2 = CreateBitmapMemoryDIB(hdcScreenCompat, oldestUndo->hDc, &lineBounds, &hdcDIBOrig, &hDibBitmap, &hDibOrigBitmap); for (int local_y = 0; local_y < lineBounds.Height; ++local_y) { @@ -5559,7 +5721,7 @@ LRESULT APIENTRY MainWndProc( // Restore area where cursor was previously RestoreCursorArea( hdcScreenCompat, hdcScreenCursorCompat, prevPt ); - + // Save area that's going to be occupied by new cursor position SaveCursorArea( hdcScreenCursorCompat, hdcScreenCompat, currentPt ); @@ -5608,12 +5770,12 @@ LRESULT APIENTRY MainWndProc( #if 0 { static int index = 0; - OutputDebug( L"%d: foreground: %x focus: %x (hwnd: %x)\n", + OutputDebug( L"%d: foreground: %x focus: %x (hwnd: %x)\n", index++, (DWORD) PtrToUlong(GetForegroundWindow()), PtrToUlong(GetFocus()), PtrToUlong(hWnd)); } #endif return TRUE; - + case WM_LBUTTONDOWN: g_StraightDirection = 0; @@ -5626,10 +5788,10 @@ LRESULT APIENTRY MainWndProc( RestoreCursorArea( hdcScreenCompat, hdcScreenCursorCompat, prevPt ); } - + // don't push undo if we sent this to ourselves for a pen resize if( wParam != -1 ) { - + PushDrawUndo( hdcScreenCompat, &drawUndoList, width, height ); } else { @@ -5658,7 +5820,7 @@ LRESULT APIENTRY MainWndProc( if( wParam & MK_SHIFT && wParam & MK_CONTROL ) g_DrawingShape = DRAW_ARROW; - else if( wParam & MK_CONTROL ) + else if( wParam & MK_CONTROL ) g_DrawingShape = DRAW_RECTANGLE; else if( wParam & MK_SHIFT ) g_DrawingShape = DRAW_LINE; @@ -5666,8 +5828,8 @@ LRESULT APIENTRY MainWndProc( g_DrawingShape = DRAW_ELLIPSE; g_RectangleAnchor.x = LOWORD(lParam); g_RectangleAnchor.y = HIWORD(lParam); - SetRect(&g_rcRectangle, g_RectangleAnchor.x, g_RectangleAnchor.y, - g_RectangleAnchor.x, g_RectangleAnchor.y); + SetRect(&g_rcRectangle, g_RectangleAnchor.x, g_RectangleAnchor.y, + g_RectangleAnchor.x, g_RectangleAnchor.y); } else { @@ -5685,10 +5847,10 @@ LRESULT APIENTRY MainWndProc( } g_Tracing = TRUE; SetROP2( hdcScreenCompat, R2_COPYPEN ); - prevPt.x = LOWORD(lParam); - prevPt.y = HIWORD(lParam); + prevPt.x = LOWORD(lParam); + prevPt.y = HIWORD(lParam); g_HaveDrawn = TRUE; - + } else { OutputDebug(L"Tracing on\n"); @@ -5779,7 +5941,7 @@ LRESULT APIENTRY MainWndProc( return TRUE; case WM_LBUTTONUP: - OutputDebug(L"LBUTTONUP: zoomed: %d drawing: %d tracing: %d\n", + OutputDebug(L"LBUTTONUP: zoomed: %d drawing: %d tracing: %d\n", g_Zoomed, g_Drawing, g_Tracing); if( g_Zoomed && g_Drawing && g_Tracing ) { @@ -5800,12 +5962,12 @@ LRESULT APIENTRY MainWndProc( if( g_StraightDirection == -1 ) { adjustPos.x = prevPt.x; - + } else { adjustPos.y = prevPt.y; } - lParam = MAKELPARAM( adjustPos.x, adjustPos.y ); + lParam = MAKELPARAM( adjustPos.x, adjustPos.y ); if( !g_DrawingShape ) { @@ -5843,7 +6005,7 @@ LRESULT APIENTRY MainWndProc( } SaveCursorArea( hdcScreenCursorCompat, hdcScreenCompat, prevPt ); DrawCursor( hdcScreenCompat, prevPt, zoomLevel, width, height ); - + } else if (g_rcRectangle.top != g_rcRectangle.bottom || g_rcRectangle.left != g_rcRectangle.right ) { @@ -5912,18 +6074,18 @@ LRESULT APIENTRY MainWndProc( DeleteTypedText( &typedKeyList ); // 1 means don't reset the cursor. We get that for font resizing - // Only move the cursor if we're drawing, because otherwise the screen moves to center + // Only move the cursor if we're drawing, because otherwise the screen moves to center // on the new cursor position if( wParam != 1 && g_Drawing ) { prevPt.x = cursorRc.left; prevPt.y = cursorRc.top; - SetCursorPos( monInfo.rcMonitor.left + prevPt.x, + SetCursorPos( monInfo.rcMonitor.left + prevPt.x, monInfo.rcMonitor.top + prevPt.y ); SaveCursorArea( hdcScreenCursorCompat, hdcScreenCompat, prevPt ); SendMessage( hWnd, WM_MOUSEMOVE, 0, MAKELPARAM( prevPt.x, prevPt.y )); - + } else if( !g_Drawing) { // FIX: would be nice to reset cursor so screen doesn't move @@ -5969,12 +6131,12 @@ LRESULT APIENTRY MainWndProc( TrackPopupMenu( hPopupMenu, 0, pt.x , pt.y, 0, hWnd, NULL ); DestroyMenu( hPopupMenu ); break; - } + } case WM_LBUTTONDBLCLK: if( !g_TimerActive ) { SendMessage( hWnd, WM_COMMAND, IDC_OPTIONS, 0 ); - + } else { SetForegroundWindow( hWnd ); @@ -6147,6 +6309,13 @@ LRESULT APIENTRY MainWndProc( 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) { // To open the PowerToys settings in the ZoomIt page. @@ -6247,7 +6416,7 @@ LRESULT APIENTRY MainWndProc( openFileName.lpstrDefExt = NULL; // "*.png"; openFileName.nFilterIndex = 1; openFileName.lpstrFilter = L"Zoomed PNG\0*.png\0" - //"Zoomed BMP\0*.bmp\0" + //"Zoomed BMP\0*.bmp\0" "Actual size PNG\0*.png\0\0"; //"Actual size BMP\0*.bmp\0\0"; openFileName.lpstrFile = filePath; @@ -6282,7 +6451,7 @@ LRESULT APIENTRY MainWndProc( 0, copyWidth, copyHeight, SRCCOPY | CAPTUREBLT ); - + SavePng( targetFilePath, hSaveBitmap ); } } @@ -6362,10 +6531,10 @@ LRESULT APIENTRY MainWndProc( monInfo.rcMonitor.left + copyX, monInfo.rcMonitor.top + copyY, copyWidth, copyHeight, - SRCCOPY|CAPTUREBLT ); + SRCCOPY|CAPTUREBLT ); if( OpenClipboard( hWnd )) { - + EmptyClipboard(); SetClipboardData( CF_BITMAP, hSaveBitmap ); CloseClipboard(); @@ -6375,7 +6544,7 @@ LRESULT APIENTRY MainWndProc( } break; - case IDC_DRAW: + case IDC_DRAW: PostMessage( hWnd, WM_HOTKEY, DRAW_HOTKEY, 1 ); break; @@ -6459,10 +6628,10 @@ LRESULT APIENTRY MainWndProc( ( !g_TimerActive || wcscmp( activeBreakBackgroundFile, g_BreakBackgroundFile ) ) ) { _tcscpy( activeBreakBackgroundFile, g_BreakBackgroundFile ); - + DeleteObject( g_hBackgroundBmp ); DeleteDC( g_hDcBackgroundFile ); - + g_hBackgroundBmp = NULL; g_hBackgroundBmp = LoadImageFile( g_BreakBackgroundFile ); if( g_hBackgroundBmp == NULL ) @@ -6504,15 +6673,15 @@ LRESULT APIENTRY MainWndProc( hNegativeTimerFont = CreateFontIndirect( &g_LogFont ); // Create backing bitmap - hdcScreenCompat = CreateCompatibleDC(hdcScreen); + hdcScreenCompat = CreateCompatibleDC(hdcScreen); bmp.bmBitsPixel = static_cast(GetDeviceCaps(hdcScreen, BITSPIXEL)); bmp.bmPlanes = static_cast(GetDeviceCaps(hdcScreen, PLANES)); bmp.bmWidth = width; bmp.bmHeight = height; - bmp.bmWidthBytes = ((bmp.bmWidth + 15) &~15)/8; - hbmpCompat = CreateBitmap(bmp.bmWidth, bmp.bmHeight, - bmp.bmPlanes, bmp.bmBitsPixel, static_cast(NULL)); - SelectObject(hdcScreenCompat, hbmpCompat); + bmp.bmWidthBytes = ((bmp.bmWidth + 15) &~15)/8; + hbmpCompat = CreateBitmap(bmp.bmWidth, bmp.bmHeight, + bmp.bmPlanes, bmp.bmBitsPixel, static_cast(NULL)); + SelectObject(hdcScreenCompat, hbmpCompat); SetTextColor( hdcScreenCompat, g_BreakPenColor ); SetBkMode( hdcScreenCompat, TRANSPARENT ); @@ -6527,7 +6696,7 @@ LRESULT APIENTRY MainWndProc( BringWindowToTop( hWnd ); SetForegroundWindow( hWnd ); SetActiveWindow( hWnd ); - SetWindowPos( hWnd, HWND_NOTOPMOST, monInfo.rcMonitor.left, monInfo.rcMonitor.top, + SetWindowPos( hWnd, HWND_NOTOPMOST, monInfo.rcMonitor.left, monInfo.rcMonitor.top, width, height, SWP_SHOWWINDOW ); } break; @@ -6535,10 +6704,10 @@ LRESULT APIENTRY MainWndProc( case IDCANCEL: memset( &tNotifyIconData, 0, sizeof(tNotifyIconData)); - tNotifyIconData.cbSize = sizeof(NOTIFYICONDATA); - tNotifyIconData.hWnd = hWnd; - tNotifyIconData.uID = 1; - Shell_NotifyIcon(NIM_DELETE, &tNotifyIconData); + tNotifyIconData.cbSize = sizeof(NOTIFYICONDATA); + tNotifyIconData.hWnd = hWnd; + tNotifyIconData.uID = 1; + Shell_NotifyIcon(NIM_DELETE, &tNotifyIconData); reg.WriteRegSettings( RegSettings ); if( hWndOptions ) @@ -6561,7 +6730,7 @@ LRESULT APIENTRY MainWndProc( if( breakTimeout == 0 && g_BreakPlaySoundFile ) { PlaySound( g_BreakSoundFile, NULL, SND_FILENAME|SND_ASYNC ); - } + } break; case 2: @@ -6579,9 +6748,9 @@ LRESULT APIENTRY MainWndProc( KillTimer( hWnd, wParam ); OutputDebug( L"SETCURSOR mon_left: %x mon_top: %x x: %d y: %d\n", monInfo.rcMonitor.left, monInfo.rcMonitor.top, cursorPos.x, cursorPos.y ); - SetCursorPos( monInfo.rcMonitor.left + cursorPos.x, + SetCursorPos( monInfo.rcMonitor.left + cursorPos.x, monInfo.rcMonitor.top + cursorPos.y ); - } + } } else { @@ -6607,13 +6776,13 @@ LRESULT APIENTRY MainWndProc( OutputDebug(L"FINAL MOUSE: x: %d y: %d\n", cursorPos.x, cursorPos.y ); GetZoomedTopLeftCoordinates(zoomLevel, &cursorPos, &x, width, &y, height); cursorPos.x = monInfo.rcMonitor.left + x + static_cast((cursorPos.x - x) * zoomLevel); - cursorPos.y = monInfo.rcMonitor.top + y + static_cast((cursorPos.y - y) * zoomLevel); + cursorPos.y = monInfo.rcMonitor.top + y + static_cast((cursorPos.y - y) * zoomLevel); SetCursorPos(cursorPos.x, cursorPos.y); } if( hTargetWindow ) { SetWindowPos( hTargetWindow, HWND_BOTTOM, rcTargetWindow.left, rcTargetWindow.top, - rcTargetWindow.right - rcTargetWindow.left, + rcTargetWindow.right - rcTargetWindow.left, rcTargetWindow.bottom - rcTargetWindow.top, 0 ); hTargetWindow = NULL; } @@ -6670,7 +6839,7 @@ LRESULT APIENTRY MainWndProc( case WM_PAINT: - hDc = BeginPaint(hWnd, &ps); + hDc = BeginPaint(hWnd, &ps); if( ( ( g_RecordCropping == FALSE ) || ( zoomLevel == 1 ) ) && g_Zoomed ) { @@ -6680,25 +6849,25 @@ LRESULT APIENTRY MainWndProc( #if SCALE_GDIPLUS if ( zoomLevel >= zoomTelescopeTarget ) { // do a high-quality render - extern void ScaleImage( HDC hdcDst, float xDst, float yDst, float wDst, float hDst, + extern void ScaleImage( HDC hdcDst, float xDst, float yDst, float wDst, float hDst, HBITMAP bmSrc, float xSrc, float ySrc, float wSrc, float hSrc ); - ScaleImage( ps.hdc, - 0, 0, - (float)bmp.bmWidth, (float)bmp.bmHeight, - hbmpCompat, - (float)x, (float)y, - width/zoomLevel, height/zoomLevel ); + ScaleImage( ps.hdc, + 0, 0, + (float)bmp.bmWidth, (float)bmp.bmHeight, + hbmpCompat, + (float)x, (float)y, + width/zoomLevel, height/zoomLevel ); } else { // do a fast, less accurate render (but use smooth if enabled) SetStretchBltMode( hDc, g_SmoothImage ? HALFTONE : COLORONCOLOR ); - StretchBlt( ps.hdc, - 0, 0, - bmp.bmWidth, bmp.bmHeight, - hdcScreenCompat, - x, y, + StretchBlt( ps.hdc, + 0, 0, + bmp.bmWidth, bmp.bmHeight, + hdcScreenCompat, + x, y, (int) (width/zoomLevel), (int) (height/zoomLevel), - SRCCOPY); + SRCCOPY); } #else #if SCALE_HALFTONE @@ -6711,13 +6880,13 @@ LRESULT APIENTRY MainWndProc( SetStretchBltMode( hDc, COLORONCOLOR ); } #endif - StretchBlt( ps.hdc, - 0, 0, - bmp.bmWidth, bmp.bmHeight, - hdcScreenCompat, - x, y, + StretchBlt( ps.hdc, + 0, 0, + bmp.bmWidth, bmp.bmHeight, + hdcScreenCompat, + x, y, static_cast(width/zoomLevel), static_cast(height/zoomLevel), - SRCCOPY|CAPTUREBLT ); + SRCCOPY|CAPTUREBLT ); #endif } else if( g_TimerActive ) { @@ -6737,7 +6906,7 @@ LRESULT APIENTRY MainWndProc( StretchBlt( hdcScreenCompat, 0, 0, width, height, g_hDcBackgroundFile, 0, 0, local_bmp.bmWidth, local_bmp.bmHeight, SRCCOPY|CAPTUREBLT ); } else { - BitBlt( hdcScreenCompat, width/2 - local_bmp.bmWidth/2, height/2 - local_bmp.bmHeight/2, + BitBlt( hdcScreenCompat, width/2 - local_bmp.bmWidth/2, height/2 - local_bmp.bmHeight/2, local_bmp.bmWidth, local_bmp.bmHeight, g_hDcBackgroundFile, 0, 0, SRCCOPY|CAPTUREBLT ); } } @@ -6746,13 +6915,13 @@ LRESULT APIENTRY MainWndProc( if( breakTimeout > 0 ) { _stprintf( timerText, L"% 2d:%02d", breakTimeout/60, breakTimeout % 60 ); - + } else { _tcscpy( timerText, L"0:00" ); } rc.left = rc.top = 0; - DrawText( hdcScreenCompat, timerText, -1, &rc, + DrawText( hdcScreenCompat, timerText, -1, &rc, DT_NOCLIP|DT_LEFT|DT_NOPREFIX|DT_CALCRECT ); rc1.left = rc1.right = rc1.bottom = rc1.top = 0; @@ -6761,7 +6930,7 @@ LRESULT APIENTRY MainWndProc( _stprintf( negativeTimerText, L"(-% 2d:%02d)", -breakTimeout/60, -breakTimeout % 60 ); HFONT prevFont = static_cast(SelectObject( hdcScreenCompat, hNegativeTimerFont )); - DrawText( hdcScreenCompat, negativeTimerText, -1, &rc1, + DrawText( hdcScreenCompat, negativeTimerText, -1, &rc1, DT_NOCLIP|DT_LEFT|DT_NOPREFIX|DT_CALCRECT ); SelectObject( hdcScreenCompat, prevFont ); } @@ -6813,7 +6982,7 @@ LRESULT APIENTRY MainWndProc( rc1.top = rc.bottom + 10; rc1.left = rc.left + ((rc.right - rc.left)-(rc1.right-rc1.left))/2; HFONT prevFont = static_cast(SelectObject( hdcScreenCompat, hNegativeTimerFont )); - DrawText( hdcScreenCompat, negativeTimerText, -1, &rc1, + DrawText( hdcScreenCompat, negativeTimerText, -1, &rc1, DT_NOCLIP|DT_LEFT|DT_NOPREFIX ); SelectObject( hdcScreenCompat, prevFont ); } @@ -6821,7 +6990,7 @@ LRESULT APIENTRY MainWndProc( // Copy to screen BitBlt( ps.hdc, 0, 0, width, height, hdcScreenCompat, 0, 0, SRCCOPY|CAPTUREBLT ); } - EndPaint(hWnd, &ps); + EndPaint(hWnd, &ps); return TRUE; case WM_DESTROY: @@ -6906,7 +7075,7 @@ LRESULT CALLBACK LiveZoomWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM if( !startedInPresentationMode ) { SetTimer( hWnd, 1, LIVEZOOM_WINDOW_TIMEOUT, NULL ); - } + } } break; @@ -6934,7 +7103,7 @@ LRESULT CALLBACK LiveZoomWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM UpdateWindow(hWnd); } - // Are we coming back from a static zoom that + // Are we coming back from a static zoom that // was started while we were live zoomed? if( g_ZoomOnLiveZoom ) { @@ -6974,7 +7143,7 @@ LRESULT CALLBACK LiveZoomWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM SendMessage( hWnd, WM_TIMER, 0, 0); SetTimer( hWnd, 0, ZOOM_LEVEL_STEP_TIME, NULL ); - + } else { KillTimer( hWnd, 0 ); @@ -7012,14 +7181,14 @@ LRESULT CALLBACK LiveZoomWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM GetCursorPos(&cursorPos); - // Reclaim topmost status, to prevent unmagnified menus from remaining in view. + // Reclaim topmost status, to prevent unmagnified menus from remaining in view. memset(&matrix, 0, sizeof(matrix)); if( !g_fullScreenWorkaround ) { pSetLayeredWindowAttributes( hWnd, 0, 255, LWA_ALPHA ); SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE); - + OutputDebug(L"LIVEZOOM RECLAIM\n"); } @@ -7028,7 +7197,7 @@ LRESULT CALLBACK LiveZoomWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM moveWidth = sourceRectWidth/LIVEZOOM_MOVE_REGIONS; moveHeight = sourceRectHeight/LIVEZOOM_MOVE_REGIONS; curTickCount = GetTickCount(); - if( zoomLevel != zoomTelescopeTarget && + if( zoomLevel != zoomTelescopeTarget && (prevZoomStepTickCount == 0 || (curTickCount - prevZoomStepTickCount > ZOOM_LEVEL_STEP_TIME)) ) { prevZoomStepTickCount = curTickCount; @@ -7040,7 +7209,7 @@ LRESULT CALLBACK LiveZoomWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM } else { zoomLevel *= zoomTelescopeStep; - } + } // Time to exit zoom mode? if( zoomTelescopeTarget == 1 && zoomLevel == 1 ) { @@ -7059,13 +7228,13 @@ LRESULT CALLBACK LiveZoomWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM matrix.v[1][2] = (static_cast(-lastSourceRect.top) * zoomLevel ); matrix.v[2][2] = 1.0f; } - + // // Pre-adjust for monitor boundary // adjustedCursorPos.x = cursorPos.x - monInfo.rcMonitor.left; adjustedCursorPos.y = cursorPos.y - monInfo.rcMonitor.top; - GetZoomedTopLeftCoordinates( zoomLevel, &adjustedCursorPos, reinterpret_cast(&zoomCenterPos.x), width, + GetZoomedTopLeftCoordinates( zoomLevel, &adjustedCursorPos, reinterpret_cast(&zoomCenterPos.x), width, reinterpret_cast(&zoomCenterPos.y), height ); // @@ -7078,11 +7247,11 @@ LRESULT CALLBACK LiveZoomWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM int xOffset = cursorPos.x - lastSourceRect.left; int yOffset = cursorPos.y - lastSourceRect.top; - zoomCenterPos.x = 0; - zoomCenterPos.y = 0; - if( xOffset < moveWidth ) + zoomCenterPos.x = 0; + zoomCenterPos.y = 0; + if( xOffset < moveWidth ) zoomCenterPos.x = lastSourceRect.left + sourceRectWidth/2 - (moveWidth - xOffset); - else if( xOffset > moveWidth * (LIVEZOOM_MOVE_REGIONS-1) ) + else if( xOffset > moveWidth * (LIVEZOOM_MOVE_REGIONS-1) ) zoomCenterPos.x = lastSourceRect.left + sourceRectWidth/2 + (xOffset - moveWidth*(LIVEZOOM_MOVE_REGIONS-1)); if( yOffset < moveHeight ) zoomCenterPos.y = lastSourceRect.top + sourceRectHeight/2 - (moveHeight - yOffset); @@ -7090,8 +7259,8 @@ LRESULT CALLBACK LiveZoomWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM zoomCenterPos.y = lastSourceRect.top + sourceRectHeight/2 + (yOffset - moveHeight*(LIVEZOOM_MOVE_REGIONS-1)); } if( matrix.v[0][0] || zoomCenterPos.x || zoomCenterPos.y ) { - - if( zoomCenterPos.y == 0 ) + + if( zoomCenterPos.y == 0 ) zoomCenterPos.y = lastSourceRect.top + sourceRectHeight/2; if( zoomCenterPos.x == 0 ) zoomCenterPos.x = lastSourceRect.left + sourceRectWidth/2; @@ -7102,14 +7271,14 @@ LRESULT CALLBACK LiveZoomWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM sourceRect.top = zoomCenterPos.y - zoomHeight / 2; // Don't scroll outside desktop area. - if (sourceRect.left < monInfo.rcMonitor.left) + if (sourceRect.left < monInfo.rcMonitor.left) sourceRect.left = monInfo.rcMonitor.left; else if (sourceRect.left > monInfo.rcMonitor.right - zoomWidth ) sourceRect.left = monInfo.rcMonitor.right - zoomWidth; sourceRect.right = sourceRect.left + zoomWidth; - if (sourceRect.top < monInfo.rcMonitor.top) + if (sourceRect.top < monInfo.rcMonitor.top) sourceRect.top = monInfo.rcMonitor.top; - else if (sourceRect.top > monInfo.rcMonitor.bottom - zoomHeight) + else if (sourceRect.top > monInfo.rcMonitor.bottom - zoomHeight) sourceRect.top = monInfo.rcMonitor.bottom - zoomHeight; sourceRect.bottom = sourceRect.top + zoomHeight; @@ -7194,7 +7363,7 @@ LRESULT CALLBACK LiveZoomWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM DestroyWindow( hWnd ); } - } + } } break; } @@ -7207,9 +7376,9 @@ LRESULT CALLBACK LiveZoomWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM // Existing presentation mode DestroyWindow( hWnd ); - + } else if( !startedInPresentationMode && IsPresentationMode()) { - + // Kill the timer if one was configured, because now // we're going to go away when they exit presentation mode KillTimer( hWnd, 1 ); @@ -7221,19 +7390,19 @@ LRESULT CALLBACK LiveZoomWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM float newZoomLevel = zoomLevel; switch( wParam ) { case 0: - // zoom in - if( newZoomLevel < ZOOM_LEVEL_MAX ) + // zoom in + if( newZoomLevel < ZOOM_LEVEL_MAX ) newZoomLevel *= 2; zoomTelescopeStep = ZOOM_LEVEL_STEP_IN; break; case 1: - if( newZoomLevel > 2 ) + if( newZoomLevel > 2 ) newZoomLevel /= 2; else { - newZoomLevel *= .75; - if( newZoomLevel < ZOOM_LEVEL_MIN ) + newZoomLevel *= .75; + if( newZoomLevel < ZOOM_LEVEL_MIN ) newZoomLevel = ZOOM_LEVEL_MIN; } zoomTelescopeStep = ZOOM_LEVEL_STEP_OUT; @@ -7260,12 +7429,12 @@ LRESULT CALLBACK LiveZoomWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM break; case VK_UP: - SendMessage( hWnd, WM_MOUSEWHEEL, + SendMessage( hWnd, WM_MOUSEWHEEL, MAKEWPARAM( GetAsyncKeyState( VK_LCONTROL ) != 0 ? MK_CONTROL: 0, WHEEL_DELTA), 0 ); return TRUE; case VK_DOWN: - SendMessage( hWnd, WM_MOUSEWHEEL, + SendMessage( hWnd, WM_MOUSEWHEEL, MAKEWPARAM( GetAsyncKeyState( VK_LCONTROL ) != 0 ? MK_CONTROL: 0, -WHEEL_DELTA), 0 ); return TRUE; } @@ -7273,10 +7442,10 @@ LRESULT CALLBACK LiveZoomWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM case WM_DESTROY: g_hWndLiveZoom = NULL; break; - + case WM_SIZE: GetClientRect(hWnd, &rc); - SetWindowPos(g_hWndLiveZoomMag, NULL, + SetWindowPos(g_hWndLiveZoomMag, NULL, rc.left, rc.top, rc.right, rc.bottom, 0 ); break; @@ -7352,7 +7521,7 @@ LRESULT CALLBACK LiveZoomWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM default: return DefWindowProc(hWnd, message, wParam, lParam); } - return 0; + return 0; } @@ -7407,7 +7576,7 @@ HRESULT __stdcall WrapD3D11CreateDevice( // InitInstance // //---------------------------------------------------------------------------- -HWND InitInstance( HINSTANCE hInstance, int nCmdShow ) +HWND InitInstance( HINSTANCE hInstance, int nCmdShow ) { WNDCLASS wcZoomIt; HWND hWndMain; @@ -7423,7 +7592,7 @@ HWND InitInstance( HINSTANCE hInstance, int nCmdShow ) wcZoomIt.cbClsExtra = 0; wcZoomIt.cbWndExtra = 0; wcZoomIt.hInstance = hInstance; - wcZoomIt.hIcon = 0; + wcZoomIt.hIcon = 0; wcZoomIt.hCursor = LoadCursor(NULL, IDC_ARROW); wcZoomIt.hbrBackground = NULL; wcZoomIt.lpszMenuName = NULL; @@ -7435,40 +7604,40 @@ HWND InitInstance( HINSTANCE hInstance, int nCmdShow ) g_LiveZoomToggleKey = 0; } - wcZoomIt.style = 0; - wcZoomIt.lpfnWndProc = (WNDPROC)MainWndProc; - wcZoomIt.cbClsExtra = 0; - wcZoomIt.cbWndExtra = 0; + wcZoomIt.style = 0; + wcZoomIt.lpfnWndProc = (WNDPROC)MainWndProc; + wcZoomIt.cbClsExtra = 0; + wcZoomIt.cbWndExtra = 0; wcZoomIt.hInstance = hInstance; wcZoomIt.hIcon = NULL; wcZoomIt.hCursor = LoadCursor( hInstance, L"NULLCURSOR" ); wcZoomIt.hbrBackground = NULL; - wcZoomIt.lpszMenuName = NULL; + wcZoomIt.lpszMenuName = NULL; wcZoomIt.lpszClassName = L"ZoomitClass"; if ( ! RegisterClass(&wcZoomIt) ) return FALSE; - hWndMain = CreateWindowEx( WS_EX_TOOLWINDOW, L"ZoomitClass", - L"Zoomit Zoom Window", + hWndMain = CreateWindowEx( WS_EX_TOOLWINDOW, L"ZoomitClass", + L"Zoomit Zoom Window", WS_POPUP, - 0, 0, 0, 0, - NULL, - NULL, - hInstance, + 0, 0, + NULL, + NULL, + hInstance, NULL); - // If window could not be created, return "failure" + // If window could not be created, return "failure" if (!hWndMain ) return NULL; - // Make the window visible; update its client area; and return "success" + // Make the window visible; update its client area; and return "success" ShowWindow(hWndMain, SW_HIDE); // Add tray icon EnableDisableTrayIcon( hWndMain, g_ShowTrayIcon ); - return hWndMain; + return hWndMain; -} +} //---------------------------------------------------------------------------- // @@ -7478,7 +7647,7 @@ HWND InitInstance( HINSTANCE hInstance, int nCmdShow ) int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ PWSTR lpCmdLine, _In_ int nCmdShow ) { - MSG msg; + MSG msg; HACCEL hAccel; if( !ShowEula( APPNAME, NULL, NULL )) return 1; @@ -7541,7 +7710,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance if( !CreateEvent( NULL, FALSE, FALSE, _T("Local\\ZoomitActive"))) { CreateEvent( NULL, FALSE, FALSE, _T("ZoomitActive")); - } + } if( GetLastError() == ERROR_ALREADY_EXISTS ) { if (g_StartedByPowerToys) { @@ -7562,7 +7731,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance if( local_hWndOptions ) { SetForegroundWindow( local_hWndOptions ); - SetWindowPos( local_hWndOptions, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE|SWP_NOMOVE|SWP_SHOWWINDOW ); + SetWindowPos( local_hWndOptions, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE|SWP_NOMOVE|SWP_SHOWWINDOW ); break; } Sleep( 100 ); diff --git a/src/modules/ZoomIt/ZoomIt/resource.h b/src/modules/ZoomIt/ZoomIt/resource.h index 568eebda4b..2458e8ce75 100644 --- a/src/modules/ZoomIt/ZoomIt/resource.h +++ b/src/modules/ZoomIt/ZoomIt/resource.h @@ -93,6 +93,7 @@ #define IDC_DEMOTYPE_SLIDER2 1074 #define IDC_DEMOTYPE_STATIC2 1074 #define IDC_COPYRIGHT 1075 +#define IDC_RECORD_FORMAT 1076 #define IDC_PEN_WIDTH 1105 #define IDC_TIMER 1106 #define IDC_SMOOTH_IMAGE 1107 diff --git a/src/modules/ZoomIt/ZoomItSettingsInterop/ZoomItSettings.cpp b/src/modules/ZoomIt/ZoomItSettingsInterop/ZoomItSettings.cpp index 3eb3e235da..25d9678ef2 100644 --- a/src/modules/ZoomIt/ZoomItSettingsInterop/ZoomItSettings.cpp +++ b/src/modules/ZoomIt/ZoomItSettingsInterop/ZoomItSettings.cpp @@ -14,6 +14,9 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation const unsigned int SPECIAL_SEMANTICS_SHORTCUT = 1; const unsigned int SPECIAL_SEMANTICS_COLOR = 2; 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 base64_decode(const std::wstring& base64_string) { @@ -72,6 +75,9 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation { L"PenColor", SPECIAL_SEMANTICS_COLOR }, { L"BreakPenColor", SPECIAL_SEMANTICS_COLOR }, { 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() @@ -103,6 +109,11 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation value & 0xFF); _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) { /* PowerToys settings likes colors as #FFFFFF strings. @@ -156,6 +167,9 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation curSetting++; } + DWORD recordScaling = (g_RecordingFormat == static_cast(0)) ? g_RecordScalingGIF : g_RecordScalingMP4; + _settings.add_property(L"RecordScaling", recordScaling); + return _settings.get_raw_json().Stringify(); } @@ -167,6 +181,8 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation PowerToysSettings::PowerToyValues valuesFromSettings = PowerToysSettings::PowerToyValues::from_json_string(json, L"ZoomIt"); + bool formatChanged = false; + PREG_SETTING curSetting = RegSettings; while (curSetting->ValueName) { @@ -212,6 +228,42 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation *static_cast(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(formatValue); + + *static_cast(curSetting->Setting) = formatValue; + + if (oldFormat != newFormat) + { + formatChanged = true; + + if (oldFormat == static_cast(0)) + { + g_RecordScalingGIF = g_RecordScaling; + } + else + { + g_RecordScalingMP4 = g_RecordScaling; + } + + if (newFormat == static_cast(0)) + { + g_RecordScaling = g_RecordScalingGIF; + } + else + { + g_RecordScaling = g_RecordScalingMP4; + } + } + } + } else if (special_semantics->second == SPECIAL_SEMANTICS_COLOR) { /* PowerToys settings likes colors as #FFFFFF strings. @@ -275,6 +327,22 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation } curSetting++; } + + auto recordScalingValue = valuesFromSettings.get_uint_value(L"RecordScaling"); + if (recordScalingValue.has_value() && !formatChanged) + { + g_RecordScaling = recordScalingValue.value(); + + if (g_RecordingFormat == static_cast(0)) + { + g_RecordScalingGIF = recordScalingValue.value(); + } + else + { + g_RecordScalingMP4 = recordScalingValue.value(); + } + } + reg.WriteRegSettings(RegSettings); } } diff --git a/src/modules/awake/Awake/Core/Native/Bridge.cs b/src/modules/awake/Awake/Core/Native/Bridge.cs index 44812a4ef7..e82b698a47 100644 --- a/src/modules/awake/Awake/Core/Native/Bridge.cs +++ b/src/modules/awake/Awake/Core/Native/Bridge.cs @@ -88,9 +88,6 @@ namespace Awake.Core.Native [return: MarshalAs(UnmanagedType.Bool)] 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)] internal static extern bool GetMessage(out Msg lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax); diff --git a/src/modules/awake/Awake/Core/TrayHelper.cs b/src/modules/awake/Awake/Core/TrayHelper.cs index 37e2de8e48..16cd5f5795 100644 --- a/src/modules/awake/Awake/Core/TrayHelper.cs +++ b/src/modules/awake/Awake/Core/TrayHelper.cs @@ -61,9 +61,8 @@ namespace Awake.Core 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.ScreenToClient(hWnd, ref cursorPos); // Set menu information MenuInfo menuInfo = new() diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/GlobalErrorHandler.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/GlobalErrorHandler.cs index 0b55b03615..9b5ac21364 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/GlobalErrorHandler.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/GlobalErrorHandler.cs @@ -51,10 +51,10 @@ internal sealed partial class GlobalErrorHandler // without its exception being observed. It is NOT raised immediately // when the Task faults; timing depends on GC finalization. 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); @@ -70,10 +70,25 @@ internal sealed partial class GlobalErrorHandler 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( HWND.Null, - "Command Palette has encountered a fatal error and must close.\n\nAn error report has been saved to your desktop.", - "Unhandled Error", + message, + caption, MESSAGEBOX_STYLE.MB_ICONERROR); } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs index 6ec96dddfe..32f542fc3b 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs @@ -68,7 +68,6 @@ public sealed partial class MainWindow : WindowEx, public MainWindow() { InitializeComponent(); - HideWindow(); _hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32()); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw b/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw index 13cda5925b..8d15c39b39 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw @@ -431,8 +431,8 @@ Right-click to remove the key combination, thereby deactivating the shortcut. Last Position Reopen the window where it was last closed - - + + Settings @@ -493,28 +493,34 @@ Right-click to remove the key combination, thereby deactivating the shortcut. Reloading extensions.. - + Discover more extensions - + Find more extensions on the Microsoft Store or WinGet. - + Learn how to create your own extensions - + Find extensions on the Microsoft Store - + Microsoft Store - + Find extensions on WinGet - + Microsoft Store - + Search extensions + + Command Palette has encountered a fatal error and must close. + + + Command Palette - Fatal error + \ No newline at end of file diff --git a/src/modules/cmdpal/custom.props b/src/modules/cmdpal/custom.props index 86541e31cc..2cfa0bbf4b 100644 --- a/src/modules/cmdpal/custom.props +++ b/src/modules/cmdpal/custom.props @@ -5,7 +5,7 @@ true 2025 0 - 6 + 7 Microsoft Command Palette diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs index d97bde7037..e99ffae352 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs @@ -15,8 +15,6 @@ namespace Microsoft.CmdPal.Ext.Apps.Programs; public sealed partial class AppListItem : ListItem { - private static readonly Tag _appTag = new("App"); - private readonly AppCommand _appCommand; private readonly AppItem _app; private readonly Lazy
_details; @@ -48,7 +46,6 @@ public sealed partial class AppListItem : ListItem _app = app; Title = app.Name; Subtitle = app.Subtitle; - Tags = [_appTag]; Icon = Icons.GenericAppIcon; MoreCommands = AddPinCommands(_app.Commands!, isPinned); diff --git a/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPageWithDetails.cs b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPageWithDetails.cs index 018c11720c..abf80e3d1e 100644 --- a/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPageWithDetails.cs +++ b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPageWithDetails.cs @@ -63,7 +63,7 @@ internal sealed partial class SampleListPageWithDetails : ListPage Details = new Details() { 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", }, }, diff --git a/src/modules/imageresizer/tests/Models/ResizeBatchTests.cs b/src/modules/imageresizer/tests/Models/ResizeBatchTests.cs index 36a17ceb19..f45fc28e6a 100644 --- a/src/modules/imageresizer/tests/Models/ResizeBatchTests.cs +++ b/src/modules/imageresizer/tests/Models/ResizeBatchTests.cs @@ -10,7 +10,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; - +using ImageResizer.Properties; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Moq.Protected; @@ -101,7 +101,9 @@ namespace ImageResizer.Models private static ResizeBatch CreateBatch(Action executeAction) { var mock = new Mock { CallBase = true }; - mock.Protected().Setup("Execute", ItExpr.IsAny()).Callback(executeAction); + mock.Protected() + .Setup("Execute", ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((string file, Settings settings) => executeAction(file)); return mock.Object; } diff --git a/src/modules/imageresizer/ui/Models/ResizeBatch.cs b/src/modules/imageresizer/ui/Models/ResizeBatch.cs index 1181395c09..87e0b84e7b 100644 --- a/src/modules/imageresizer/ui/Models/ResizeBatch.cs +++ b/src/modules/imageresizer/ui/Models/ResizeBatch.cs @@ -87,9 +87,14 @@ namespace ImageResizer.Models public IEnumerable Process(Action reportProgress, CancellationToken cancellationToken) { double total = Files.Count; - var completed = 0; + int completed = 0; var errors = new ConcurrentBag(); + // 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 // APIs and a custom SynchronizationContext Parallel.ForEach( @@ -97,13 +102,12 @@ namespace ImageResizer.Models new ParallelOptions { CancellationToken = cancellationToken, - MaxDegreeOfParallelism = Environment.ProcessorCount, }, (file, state, i) => { try { - Execute(file); + Execute(file, settings); } catch (Exception ex) { @@ -111,14 +115,13 @@ namespace ImageResizer.Models } Interlocked.Increment(ref completed); - reportProgress(completed, total); }); return errors; } - protected virtual void Execute(string file) - => new ResizeOperation(file, DestinationDirectory, Settings.Default).Execute(); + protected virtual void Execute(string file, Settings settings) + => new ResizeOperation(file, DestinationDirectory, settings).Execute(); } } diff --git a/src/modules/imageresizer/ui/Properties/Settings.cs b/src/modules/imageresizer/ui/Properties/Settings.cs index debb26a191..0f8690dcbb 100644 --- a/src/modules/imageresizer/ui/Properties/Settings.cs +++ b/src/modules/imageresizer/ui/Properties/Settings.cs @@ -461,33 +461,42 @@ namespace ImageResizer.Properties { } - // Needs to be called on the App UI thread as the properties are bound to the UI. - App.Current.Dispatcher.Invoke(() => + if (App.Current?.Dispatcher != null) { - 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); - } - }); + // Needs to be called on the App UI thread as the properties are bound to the UI. + App.Current.Dispatcher.Invoke(() => ReloadCore(jsonSettings)); + } + else + { + ReloadCore(jsonSettings); + } _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); + } + } } } diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/AudioPreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/AudioPreviewer.cs index 586b92ed75..a3721a04ec 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/AudioPreviewer.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/AudioPreviewer.cs @@ -24,13 +24,15 @@ using Windows.Storage; namespace Peek.FilePreviewer.Previewers.MediaPreviewer { - public partial class AudioPreviewer : ObservableObject, IAudioPreviewer + public partial class AudioPreviewer : ObservableObject, IDisposable, IAudioPreviewer { + private MediaSource? _mediaSource; + [ObservableProperty] private PreviewState _state; [ObservableProperty] - private AudioPreviewData _preview; + private AudioPreviewData? _preview; private IFileSystemItem Item { get; } @@ -40,7 +42,6 @@ namespace Peek.FilePreviewer.Previewers.MediaPreviewer { Item = file; Dispatcher = DispatcherQueue.GetForCurrentThread(); - Preview = new AudioPreviewData(); } public async Task CopyAsync() @@ -63,19 +64,23 @@ namespace Peek.FilePreviewer.Previewers.MediaPreviewer { State = PreviewState.Loading; + Preview = new AudioPreviewData(); + var thumbnailTask = LoadThumbnailAsync(cancellationToken); var sourceTask = LoadSourceAsync(cancellationToken); var metadataTask = LoadMetadataAsync(cancellationToken); 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 { - State = PreviewState.Loaded; + // Release all resources on error. + Unload(); + State = PreviewState.Error; } } @@ -88,12 +93,15 @@ namespace Peek.FilePreviewer.Previewers.MediaPreviewer { cancellationToken.ThrowIfCancellationRequested(); - var thumbnail = await ThumbnailHelper.GetThumbnailAsync(Item.Path, cancellationToken) - ?? await ThumbnailHelper.GetIconAsync(Item.Path, cancellationToken); + if (Preview != null) + { + 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(); - 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(() => { + if (Preview == null) + { + return; + } + cancellationToken.ThrowIfCancellationRequested(); Preview.Title = PropertyStoreHelper.TryGetStringProperty(Item.Path, PropertyKey.MusicTitle) ?? Item.Name[..^Item.Extension.Length]; @@ -160,6 +177,22 @@ namespace Peek.FilePreviewer.Previewers.MediaPreviewer return _supportedFileTypes.Contains(item.Extension); } + public void Dispose() + { + Unload(); + GC.SuppressFinalize(this); + } + + /// + /// Explicitly unloads the preview and releases file resources. + /// + public void Unload() + { + _mediaSource?.Dispose(); + _mediaSource = null; + Preview = null; + } + private static readonly HashSet _supportedFileTypes = new() { ".aac", diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/VideoPreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/VideoPreviewer.cs index b9de53e87b..061d3eca47 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/VideoPreviewer.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/VideoPreviewer.cs @@ -25,6 +25,8 @@ namespace Peek.FilePreviewer.Previewers { public partial class VideoPreviewer : ObservableObject, IVideoPreviewer, IDisposable { + private MediaSource? _mediaSource; + [ObservableProperty] private MediaSource? preview; @@ -56,6 +58,7 @@ namespace Peek.FilePreviewer.Previewers public void Dispose() { + Unload(); GC.SuppressFinalize(this); } @@ -145,7 +148,8 @@ namespace Peek.FilePreviewer.Previewers MissingCodecName = missingCodecName; } - Preview = MediaSource.CreateFromStorageFile(storageFile); + _mediaSource = MediaSource.CreateFromStorageFile(storageFile); + Preview = _mediaSource; }); }); } @@ -155,6 +159,16 @@ namespace Peek.FilePreviewer.Previewers return !(VideoTask?.Result ?? true); } + /// + /// Explicitly unloads the preview and releases file resources. + /// + public void Unload() + { + _mediaSource?.Dispose(); + _mediaSource = null; + Preview = null; + } + private static readonly HashSet _supportedFileTypes = new() { ".mp4", ".3g2", ".3gp", ".3gp2", ".3gpp", ".asf", ".avi", ".m2t", ".m2ts", diff --git a/src/modules/peek/Peek.UITests/PeekFilePreviewTests.cs b/src/modules/peek/Peek.UITests/PeekFilePreviewTests.cs index 36f2491fcf..fd57c444ca 100644 --- a/src/modules/peek/Peek.UITests/PeekFilePreviewTests.cs +++ b/src/modules/peek/Peek.UITests/PeekFilePreviewTests.cs @@ -9,6 +9,7 @@ using System.Drawing; using System.IO; using System.Linq; using System.Runtime.InteropServices; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; 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 properties; + + if (settings.TryGetValue("properties", out var propertiesObj)) + { + if (propertiesObj is Dictionary dict) + { + properties = dict; + } + else if (propertiesObj is JsonElement jsonElem) + { + properties = JsonSerializer.Deserialize>(jsonElem.GetRawText()) + ?? throw new InvalidOperationException("Failed to deserialize properties"); + } + else + { + properties = new Dictionary(); + } + } + else + { + properties = new Dictionary(); + } + + // Update the required properties + properties["ActivationShortcut"] = new Dictionary + { + { "win", false }, + { "ctrl", true }, + { "alt", false }, + { "shift", false }, + { "code", 32 }, + { "key", "Space" }, + }; + + properties["EnableSpaceToActivate"] = new Dictionary + { + { "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] public void TestInitialize() { diff --git a/src/modules/poweraccent/PowerAccent.Core/Languages.cs b/src/modules/poweraccent/PowerAccent.Core/Languages.cs index 6e329ffe7f..06c3a2bea3 100644 --- a/src/modules/poweraccent/PowerAccent.Core/Languages.cs +++ b/src/modules/poweraccent/PowerAccent.Core/Languages.cs @@ -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_M => new[] { "ṁ" }, LetterKey.VK_N => new[] { "ņ", "ṅ", "ⁿ", "ℕ", "№" }, - LetterKey.VK_O => new[] { "ȯ", "∅" }, + LetterKey.VK_O => new[] { "ȯ", "∅", "⌀" }, LetterKey.VK_P => new[] { "ṗ", "℗", "∏", "¶" }, LetterKey.VK_Q => new[] { "ℚ" }, LetterKey.VK_R => new[] { "ṙ", "®", "ℝ" }, diff --git a/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.vcxproj b/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.vcxproj index 3afb41f546..0bfb8d2db1 100644 --- a/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.vcxproj +++ b/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.vcxproj @@ -71,6 +71,7 @@ Console + windowscodecs.lib;%(AdditionalDependencies) diff --git a/src/runner/runner.vcxproj b/src/runner/runner.vcxproj index afff599d8e..1eae5a3573 100644 --- a/src/runner/runner.vcxproj +++ b/src/runner/runner.vcxproj @@ -28,6 +28,7 @@ v143 None true + true diff --git a/src/settings-ui/Settings.UI.Library/AIServiceTypeRegistry.cs b/src/settings-ui/Settings.UI.Library/AIServiceTypeRegistry.cs index d08f2a1266..c967f5d840 100644 --- a/src/settings-ui/Settings.UI.Library/AIServiceTypeRegistry.cs +++ b/src/settings-ui/Settings.UI.Library/AIServiceTypeRegistry.cs @@ -56,9 +56,9 @@ public static class AIServiceTypeRegistry IsOnlineService = true, LegalDescription = "AdvancedPaste_Google_LegalDescription", 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", - PrivacyUri = new Uri("https://policies.google.com/privacy"), + PrivacyUri = new Uri("https://support.google.com/gemini/answer/13594961"), }, [AIServiceType.Mistral] = new AIServiceTypeMetadata { @@ -93,9 +93,9 @@ public static class AIServiceTypeRegistry IsLocalModel = true, LegalDescription = "AdvancedPaste_LocalModel_LegalDescription", TermsLabel = "AdvancedPaste_Ollama_TermsLabel", - TermsUri = new Uri("https://ollama.com/terms"), + TermsUri = new Uri("https://ollama.org/terms"), PrivacyLabel = "AdvancedPaste_Ollama_PrivacyLabel", - PrivacyUri = new Uri("https://ollama.com/privacy"), + PrivacyUri = new Uri("https://ollama.org/privacy"), }, [AIServiceType.Onnx] = new AIServiceTypeMetadata { diff --git a/src/settings-ui/Settings.UI.Library/ZoomItProperties.cs b/src/settings-ui/Settings.UI.Library/ZoomItProperties.cs index 9325907a72..1bca1b573a 100644 --- a/src/settings-ui/Settings.UI.Library/ZoomItProperties.cs +++ b/src/settings-ui/Settings.UI.Library/ZoomItProperties.cs @@ -87,6 +87,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library public IntProperty RecordScaling { get; set; } + public StringProperty RecordFormat { get; set; } + public BoolProperty CaptureAudio { get; set; } public StringProperty MicrophoneDeviceId { get; set; } diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Icons/CursorWrap.png b/src/settings-ui/Settings.UI/Assets/Settings/Icons/CursorWrap.png index 4374dfdc82..c32f1e309a 100644 Binary files a/src/settings-ui/Settings.UI/Assets/Settings/Icons/CursorWrap.png and b/src/settings-ui/Settings.UI/Assets/Settings/Icons/CursorWrap.png differ diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Icons/FindMyMouse.png b/src/settings-ui/Settings.UI/Assets/Settings/Icons/FindMyMouse.png index 6cdb55cb66..581c317518 100644 Binary files a/src/settings-ui/Settings.UI/Assets/Settings/Icons/FindMyMouse.png and b/src/settings-ui/Settings.UI/Assets/Settings/Icons/FindMyMouse.png differ diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseHighlighter.png b/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseHighlighter.png index 7e9a2da1a8..69ed506e99 100644 Binary files a/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseHighlighter.png and b/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseHighlighter.png differ diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseJump.png b/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseJump.png index 15568110cc..14d5e71d53 100644 Binary files a/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseJump.png and b/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseJump.png differ diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseWithoutBorders.png b/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseWithoutBorders.png index 83d1fbc553..99a8f64ed4 100644 Binary files a/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseWithoutBorders.png and b/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseWithoutBorders.png differ diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml index b071e7f6fe..44470ebbc1 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml @@ -9,7 +9,10 @@ mc:Ignorable="d"> -