diff --git a/PowerToys.sln b/PowerToys.sln
index d6372908a2..80f5b0adbb 100644
--- a/PowerToys.sln
+++ b/PowerToys.sln
@@ -468,7 +468,21 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FileLocksmithLibInterop", "
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "GPOWrapper", "src\common\GPOWrapper\GPOWrapper.vcxproj", "{E599C30B-9DC8-4E5A-BF27-93D4CCEDE788}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GPOWrapperProjection", "src\common\GPOWrapperProjection\GPOWrapperProjection.csproj", "{00EE9BA6-4E8F-43CA-960D-D4882F0FBB97}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GPOWrapperProjection", "src\common\GPOWrapperProjection\GPOWrapperProjection.csproj", "{00EE9BA6-4E8F-43CA-960D-D4882F0FBB97}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Peek", "Peek", "{17B4FA70-001E-4D33-BBBB-0D142DBC2E20}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Peek.UI.WPF", "src\modules\peek\Peek.UI.WPF\Peek.UI.WPF.csproj", "{C0240BC3-95AF-4B38-811A-76E3FD56B576}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Peek", "src\modules\peek\peek\peek.vcxproj", "{A1425B53-3D61-4679-8623-E64A0D3D0A48}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Peek.UI", "src\modules\peek\Peek.UI\Peek.UI.csproj", "{9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Peek.Common", "src\modules\peek\Peek.Common\Peek.Common.csproj", "{17A99C7C-0BFF-45BB-A9FD-63A0DDC105BB}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Peek.FilePreviewer", "src\modules\peek\Peek.FilePreviewer\Peek.FilePreviewer.csproj", "{AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WIC", "src\modules\peek\WIC\WIC.csproj", "{21B69DE5-59FD-4C5D-A142-EF1C1C430EAF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -1909,6 +1923,84 @@ Global
{00EE9BA6-4E8F-43CA-960D-D4882F0FBB97}.Release|x64.Build.0 = Release|x64
{00EE9BA6-4E8F-43CA-960D-D4882F0FBB97}.Release|x86.ActiveCfg = Release|x64
{00EE9BA6-4E8F-43CA-960D-D4882F0FBB97}.Release|x86.Build.0 = Release|x64
+ {C0240BC3-95AF-4B38-811A-76E3FD56B576}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {C0240BC3-95AF-4B38-811A-76E3FD56B576}.Debug|ARM64.Build.0 = Debug|ARM64
+ {C0240BC3-95AF-4B38-811A-76E3FD56B576}.Debug|x64.ActiveCfg = Debug|x64
+ {C0240BC3-95AF-4B38-811A-76E3FD56B576}.Debug|x64.Build.0 = Debug|x64
+ {C0240BC3-95AF-4B38-811A-76E3FD56B576}.Debug|x86.ActiveCfg = Debug|x64
+ {C0240BC3-95AF-4B38-811A-76E3FD56B576}.Debug|x86.Build.0 = Debug|x64
+ {C0240BC3-95AF-4B38-811A-76E3FD56B576}.Release|ARM64.ActiveCfg = Release|ARM64
+ {C0240BC3-95AF-4B38-811A-76E3FD56B576}.Release|ARM64.Build.0 = Release|ARM64
+ {C0240BC3-95AF-4B38-811A-76E3FD56B576}.Release|x64.ActiveCfg = Release|x64
+ {C0240BC3-95AF-4B38-811A-76E3FD56B576}.Release|x64.Build.0 = Release|x64
+ {C0240BC3-95AF-4B38-811A-76E3FD56B576}.Release|x86.ActiveCfg = Release|x64
+ {C0240BC3-95AF-4B38-811A-76E3FD56B576}.Release|x86.Build.0 = Release|x64
+ {A1425B53-3D61-4679-8623-E64A0D3D0A48}.Debug|ARM64.ActiveCfg = Debug|x64
+ {A1425B53-3D61-4679-8623-E64A0D3D0A48}.Debug|ARM64.Build.0 = Debug|x64
+ {A1425B53-3D61-4679-8623-E64A0D3D0A48}.Debug|x64.ActiveCfg = Debug|x64
+ {A1425B53-3D61-4679-8623-E64A0D3D0A48}.Debug|x64.Build.0 = Debug|x64
+ {A1425B53-3D61-4679-8623-E64A0D3D0A48}.Debug|x86.ActiveCfg = Debug|x64
+ {A1425B53-3D61-4679-8623-E64A0D3D0A48}.Debug|x86.Build.0 = Debug|x64
+ {A1425B53-3D61-4679-8623-E64A0D3D0A48}.Release|ARM64.ActiveCfg = Release|x64
+ {A1425B53-3D61-4679-8623-E64A0D3D0A48}.Release|ARM64.Build.0 = Release|x64
+ {A1425B53-3D61-4679-8623-E64A0D3D0A48}.Release|x64.ActiveCfg = Release|x64
+ {A1425B53-3D61-4679-8623-E64A0D3D0A48}.Release|x64.Build.0 = Release|x64
+ {A1425B53-3D61-4679-8623-E64A0D3D0A48}.Release|x86.ActiveCfg = Release|x64
+ {A1425B53-3D61-4679-8623-E64A0D3D0A48}.Release|x86.Build.0 = Release|x64
+ {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Debug|ARM64.Build.0 = Debug|ARM64
+ {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Debug|ARM64.Deploy.0 = Debug|ARM64
+ {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Debug|x64.ActiveCfg = Debug|x64
+ {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Debug|x64.Build.0 = Debug|x64
+ {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Debug|x64.Deploy.0 = Debug|x64
+ {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Debug|x86.ActiveCfg = Debug|x86
+ {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Debug|x86.Build.0 = Debug|x86
+ {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Debug|x86.Deploy.0 = Debug|x86
+ {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Release|ARM64.ActiveCfg = Release|ARM64
+ {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Release|ARM64.Build.0 = Release|ARM64
+ {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Release|ARM64.Deploy.0 = Release|ARM64
+ {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Release|x64.ActiveCfg = Release|x64
+ {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Release|x64.Build.0 = Release|x64
+ {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Release|x64.Deploy.0 = Release|x64
+ {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Release|x86.ActiveCfg = Release|x86
+ {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Release|x86.Build.0 = Release|x86
+ {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Release|x86.Deploy.0 = Release|x86
+ {17A99C7C-0BFF-45BB-A9FD-63A0DDC105BB}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {17A99C7C-0BFF-45BB-A9FD-63A0DDC105BB}.Debug|ARM64.Build.0 = Debug|ARM64
+ {17A99C7C-0BFF-45BB-A9FD-63A0DDC105BB}.Debug|x64.ActiveCfg = Debug|x64
+ {17A99C7C-0BFF-45BB-A9FD-63A0DDC105BB}.Debug|x64.Build.0 = Debug|x64
+ {17A99C7C-0BFF-45BB-A9FD-63A0DDC105BB}.Debug|x86.ActiveCfg = Debug|x64
+ {17A99C7C-0BFF-45BB-A9FD-63A0DDC105BB}.Debug|x86.Build.0 = Debug|x64
+ {17A99C7C-0BFF-45BB-A9FD-63A0DDC105BB}.Release|ARM64.ActiveCfg = Release|ARM64
+ {17A99C7C-0BFF-45BB-A9FD-63A0DDC105BB}.Release|ARM64.Build.0 = Release|ARM64
+ {17A99C7C-0BFF-45BB-A9FD-63A0DDC105BB}.Release|x64.ActiveCfg = Release|x64
+ {17A99C7C-0BFF-45BB-A9FD-63A0DDC105BB}.Release|x64.Build.0 = Release|x64
+ {17A99C7C-0BFF-45BB-A9FD-63A0DDC105BB}.Release|x86.ActiveCfg = Release|x64
+ {17A99C7C-0BFF-45BB-A9FD-63A0DDC105BB}.Release|x86.Build.0 = Release|x64
+ {AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}.Debug|ARM64.Build.0 = Debug|ARM64
+ {AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}.Debug|x64.ActiveCfg = Debug|x64
+ {AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}.Debug|x64.Build.0 = Debug|x64
+ {AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}.Debug|x86.ActiveCfg = Debug|x64
+ {AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}.Debug|x86.Build.0 = Debug|x64
+ {AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}.Release|ARM64.ActiveCfg = Release|ARM64
+ {AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}.Release|ARM64.Build.0 = Release|ARM64
+ {AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}.Release|x64.ActiveCfg = Release|x64
+ {AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}.Release|x64.Build.0 = Release|x64
+ {AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}.Release|x86.ActiveCfg = Release|x64
+ {AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}.Release|x86.Build.0 = Release|x64
+ {21B69DE5-59FD-4C5D-A142-EF1C1C430EAF}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {21B69DE5-59FD-4C5D-A142-EF1C1C430EAF}.Debug|ARM64.Build.0 = Debug|ARM64
+ {21B69DE5-59FD-4C5D-A142-EF1C1C430EAF}.Debug|x64.ActiveCfg = Debug|x64
+ {21B69DE5-59FD-4C5D-A142-EF1C1C430EAF}.Debug|x64.Build.0 = Debug|x64
+ {21B69DE5-59FD-4C5D-A142-EF1C1C430EAF}.Debug|x86.ActiveCfg = Debug|x64
+ {21B69DE5-59FD-4C5D-A142-EF1C1C430EAF}.Debug|x86.Build.0 = Debug|x64
+ {21B69DE5-59FD-4C5D-A142-EF1C1C430EAF}.Release|ARM64.ActiveCfg = Release|ARM64
+ {21B69DE5-59FD-4C5D-A142-EF1C1C430EAF}.Release|ARM64.Build.0 = Release|ARM64
+ {21B69DE5-59FD-4C5D-A142-EF1C1C430EAF}.Release|x64.ActiveCfg = Release|x64
+ {21B69DE5-59FD-4C5D-A142-EF1C1C430EAF}.Release|x64.Build.0 = Release|x64
+ {21B69DE5-59FD-4C5D-A142-EF1C1C430EAF}.Release|x86.ActiveCfg = Release|x64
+ {21B69DE5-59FD-4C5D-A142-EF1C1C430EAF}.Release|x86.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2071,6 +2163,13 @@ Global
{C604B37E-9D0E-4484-8778-E8B31B0E1B3A} = {AB82E5DD-C32D-4F28-9746-2C780846188E}
{E599C30B-9DC8-4E5A-BF27-93D4CCEDE788} = {1AFB6476-670D-4E80-A464-657E01DFF482}
{00EE9BA6-4E8F-43CA-960D-D4882F0FBB97} = {1AFB6476-670D-4E80-A464-657E01DFF482}
+ {17B4FA70-001E-4D33-BBBB-0D142DBC2E20} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
+ {C0240BC3-95AF-4B38-811A-76E3FD56B576} = {17B4FA70-001E-4D33-BBBB-0D142DBC2E20}
+ {A1425B53-3D61-4679-8623-E64A0D3D0A48} = {17B4FA70-001E-4D33-BBBB-0D142DBC2E20}
+ {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03} = {17B4FA70-001E-4D33-BBBB-0D142DBC2E20}
+ {17A99C7C-0BFF-45BB-A9FD-63A0DDC105BB} = {17B4FA70-001E-4D33-BBBB-0D142DBC2E20}
+ {AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC} = {17B4FA70-001E-4D33-BBBB-0D142DBC2E20}
+ {21B69DE5-59FD-4C5D-A142-EF1C1C430EAF} = {17B4FA70-001E-4D33-BBBB-0D142DBC2E20}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs
index 2a96496591..5b9491858f 100644
--- a/installer/PowerToysSetup/Product.wxs
+++ b/installer/PowerToysSetup/Product.wxs
@@ -17,6 +17,7 @@
+
@@ -53,7 +54,7 @@
-
+
@@ -542,6 +543,12 @@
+
+
+
+
+
+
@@ -1032,6 +1039,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1332,6 +1364,9 @@
+
+
+
@@ -1407,7 +1442,7 @@
-
+
@@ -1561,6 +1596,12 @@
Guid="$(var.CompGUIDPrefix)03">
+
+
+
processesToTerminate = {
+ std::array processesToTerminate = {
L"PowerToys.PowerLauncher.exe",
L"PowerToys.Settings.exe",
L"PowerToys.Awake.exe",
@@ -1239,6 +1239,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
L"PowerToys.FileLocksmithUI.exe",
L"PowerToys.ColorPickerUI.exe",
L"PowerToys.AlwaysOnTop.exe",
+ L"PowerToys.PeekUI.exe",
L"PowerToys.exe"
};
diff --git a/src/common/interop/interop.cpp b/src/common/interop/interop.cpp
index 83f48732f8..6b8e5d1c00 100644
--- a/src/common/interop/interop.cpp
+++ b/src/common/interop/interop.cpp
@@ -203,6 +203,10 @@ public
return gcnew String(CommonSharedConstants::AWAKE_EXIT_EVENT);
}
+ static String^ ShowPeekEvent() {
+ return gcnew String(CommonSharedConstants::SHOW_PEEK_SHARED_EVENT);
+ }
+
static String ^ PowerAccentExitEvent() {
return gcnew String(CommonSharedConstants::POWERACCENT_EXIT_EVENT);
}
diff --git a/src/common/interop/shared_constants.h b/src/common/interop/shared_constants.h
index 7adf617047..71381e35fe 100644
--- a/src/common/interop/shared_constants.h
+++ b/src/common/interop/shared_constants.h
@@ -44,6 +44,9 @@ namespace CommonSharedConstants
// Path to the event used by PowerOCR
const wchar_t SHOW_POWEROCR_SHARED_EVENT[] = L"Local\\PowerOCREvent-dc864e06-e1af-4ecc-9078-f98bee745e3a";
+ // Path to the event used to show Peek
+ const wchar_t SHOW_PEEK_SHARED_EVENT[] = L"Local\\ShowPeekEvent";
+
// Max DWORD for key code to disable keys.
const DWORD VK_DISABLED = 0x100;
}
diff --git a/src/modules/peek/Peek.Common/Models/File.cs b/src/modules/peek/Peek.Common/Models/File.cs
new file mode 100644
index 0000000000..58ab722a05
--- /dev/null
+++ b/src/modules/peek/Peek.Common/Models/File.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Peek.Common.Models
+{
+ using System;
+ using System.Threading.Tasks;
+ using Windows.Storage;
+
+#nullable enable
+
+ public class File
+ {
+ private StorageFile? storageFile;
+
+ public File(string path)
+ {
+ Path = path;
+ }
+
+ public string Path { get; init; }
+
+ public string Extension => System.IO.Path.GetExtension(Path);
+
+ public async Task GetStorageFileAsync()
+ {
+ if (storageFile == null)
+ {
+ storageFile = await StorageFile.GetFileFromPathAsync(Path);
+ }
+
+ return storageFile;
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.Common/Models/HResult.cs b/src/modules/peek/Peek.Common/Models/HResult.cs
new file mode 100644
index 0000000000..57768d3cdb
--- /dev/null
+++ b/src/modules/peek/Peek.Common/Models/HResult.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Peek.Common.Models
+{
+ public enum HResult
+ {
+ Ok = 0x0000,
+ False = 0x0001,
+ InvalidArguments = unchecked((int)0x80070057),
+ OutOfMemory = unchecked((int)0x8007000E),
+ NoInterface = unchecked((int)0x80004002),
+ Fail = unchecked((int)0x80004005),
+ ExtractionFailed = unchecked((int)0x8004B200),
+ ElementNotFound = unchecked((int)0x80070490),
+ TypeElementNotFound = unchecked((int)0x8002802B),
+ NoObject = unchecked((int)0x800401E5),
+ Win32ErrorCanceled = 1223,
+ Canceled = unchecked((int)0x800704C7),
+ ResourceInUse = unchecked((int)0x800700AA),
+ AccessDenied = unchecked((int)0x80030005),
+ }
+}
diff --git a/src/modules/peek/Peek.Common/Models/IShellItem.cs b/src/modules/peek/Peek.Common/Models/IShellItem.cs
new file mode 100644
index 0000000000..8fd1fa2440
--- /dev/null
+++ b/src/modules/peek/Peek.Common/Models/IShellItem.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Peek.Common.Models
+{
+ using System;
+ using System.Runtime.InteropServices;
+
+ [ComImport]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ [Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe")]
+ public interface IShellItem
+ {
+ void BindToHandler(
+ IntPtr pbc,
+ [MarshalAs(UnmanagedType.LPStruct)] Guid bhid,
+ [MarshalAs(UnmanagedType.LPStruct)] Guid riid,
+ out IntPtr ppv);
+
+ void GetParent(out IShellItem ppsi);
+
+ void GetDisplayName(Sigdn sigdnName, out IntPtr ppszName);
+
+ void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
+
+ void Compare(IShellItem psi, uint hint, out int piOrder);
+ }
+
+ public enum Sigdn : uint
+ {
+ NormalDisplay = 0,
+ ParentRelativeParsing = 0x80018001,
+ ParentRelativeForAddressBar = 0x8001c001,
+ DesktopAbsoluteParsing = 0x80028000,
+ ParentRelativeEditing = 0x80031001,
+ DesktopAbsoluteEditing = 0x8004c000,
+ FileSysPath = 0x80058000,
+ Url = 0x80068000,
+ }
+}
diff --git a/src/modules/peek/Peek.Common/Models/IShellItemImageFactory.cs b/src/modules/peek/Peek.Common/Models/IShellItemImageFactory.cs
new file mode 100644
index 0000000000..42920aabd3
--- /dev/null
+++ b/src/modules/peek/Peek.Common/Models/IShellItemImageFactory.cs
@@ -0,0 +1,50 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Peek.Common.Models
+{
+ using System;
+ using System.Runtime.InteropServices;
+
+ [ComImport]
+ [Guid("bcc18b79-ba16-442f-80c4-8a59c30c463b")]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ public interface IShellItemImageFactory
+ {
+ [PreserveSig]
+ HResult GetImage(
+ [In, MarshalAs(UnmanagedType.Struct)] NativeSize size,
+ [In] ThumbnailOptions flags,
+ [Out] out IntPtr phbm);
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct NativeSize
+ {
+ private int width;
+ private int height;
+
+ public int Width
+ {
+ set { width = value; }
+ }
+
+ public int Height
+ {
+ set { height = value; }
+ }
+ }
+
+ [Flags]
+ public enum ThumbnailOptions
+ {
+ None = 0x00,
+ BiggerSizeOk = 0x01,
+ InMemoryOnly = 0x02,
+ IconOnly = 0x04,
+ ThumbnailOnly = 0x08,
+ InCacheOnly = 0x10,
+ ScaleUp = 0x100,
+ }
+}
diff --git a/src/modules/peek/Peek.Common/Peek.Common.csproj b/src/modules/peek/Peek.Common/Peek.Common.csproj
new file mode 100644
index 0000000000..04afd55aa5
--- /dev/null
+++ b/src/modules/peek/Peek.Common/Peek.Common.csproj
@@ -0,0 +1,15 @@
+
+
+ net7.0-windows10.0.19041.0
+ 10.0.17763.0
+ Peek.Common
+ win10-x86;win10-x64;win10-arm64
+ true
+ enable
+
+
+
+
+
+
+
diff --git a/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml b/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml
new file mode 100644
index 0000000000..05dabb2a3a
--- /dev/null
+++ b/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml.cs b/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml.cs
new file mode 100644
index 0000000000..3ff252f2a4
--- /dev/null
+++ b/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml.cs
@@ -0,0 +1,89 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Peek.FilePreviewer
+{
+ using System;
+ using System.Threading.Tasks;
+ using CommunityToolkit.Mvvm.ComponentModel;
+ using CommunityToolkit.WinUI.UI.Media.Pipelines;
+ using Microsoft.UI.Xaml;
+ using Microsoft.UI.Xaml.Controls;
+ using Microsoft.UI.Xaml.Media.Imaging;
+ using Peek.Common.Models;
+ using Peek.FilePreviewer.Models;
+ using Peek.FilePreviewer.Previewers;
+ using Windows.Foundation;
+
+ [INotifyPropertyChanged]
+ public sealed partial class FilePreview : UserControl
+ {
+ public event EventHandler? PreviewSizeChanged;
+
+ public static readonly DependencyProperty FilesProperty =
+ DependencyProperty.Register(
+ nameof(File),
+ typeof(File),
+ typeof(FilePreview),
+ new PropertyMetadata(false, async (d, e) => await ((FilePreview)d).OnFilePropertyChanged()));
+
+ [ObservableProperty]
+ private ImagePreviewer? previewer;
+
+ public FilePreview()
+ {
+ InitializeComponent();
+ }
+
+ public File File
+ {
+ get => (File)GetValue(FilesProperty);
+ set => SetValue(FilesProperty, value);
+ }
+
+ public bool IsPreviewLoading(BitmapSource? bitmapSource)
+ {
+ return bitmapSource == null;
+ }
+
+ private async Task OnFilePropertyChanged()
+ {
+ if (File == null)
+ {
+ return;
+ }
+
+ // TODO: Implement plugin pattern to support any file types.
+ if (IsSupportedImage(File.Extension))
+ {
+ Previewer = new ImagePreviewer(File);
+ var size = await Previewer.GetPreviewSizeAsync();
+ PreviewSizeChanged?.Invoke(this, new PreviewSizeChangedArgs(size));
+ await Previewer.LoadPreviewAsync();
+ }
+ else
+ {
+ Previewer = null;
+ PreviewSizeChanged?.Invoke(this, new PreviewSizeChangedArgs(new Size(1280, 720)));
+ }
+ }
+
+ // TODO: Find all supported file types for the image previewer
+ private static bool IsSupportedImage(string extension) => extension switch
+ {
+ ".bmp" => true,
+ ".gif" => true,
+ ".jpg" => true,
+ ".jfif" => true,
+ ".jfi" => true,
+ ".jif" => true,
+ ".jpeg" => true,
+ ".jpe" => true,
+ ".png" => true,
+ ".tif" => true,
+ ".tiff" => true,
+ _ => false,
+ };
+ }
+}
diff --git a/src/modules/peek/Peek.FilePreviewer/Models/PreviewSizeChangedArgs.cs b/src/modules/peek/Peek.FilePreviewer/Models/PreviewSizeChangedArgs.cs
new file mode 100644
index 0000000000..a34785e7cb
--- /dev/null
+++ b/src/modules/peek/Peek.FilePreviewer/Models/PreviewSizeChangedArgs.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Peek.FilePreviewer.Models
+{
+ using Windows.Foundation;
+
+ public class PreviewSizeChangedArgs
+ {
+ public PreviewSizeChangedArgs(Size windowSizeRequested)
+ {
+ WindowSizeRequested = windowSizeRequested;
+ }
+
+ public Size WindowSizeRequested { get; init; }
+ }
+}
diff --git a/src/modules/peek/Peek.FilePreviewer/Peek.FilePreviewer.csproj b/src/modules/peek/Peek.FilePreviewer/Peek.FilePreviewer.csproj
new file mode 100644
index 0000000000..ac6bcb542e
--- /dev/null
+++ b/src/modules/peek/Peek.FilePreviewer/Peek.FilePreviewer.csproj
@@ -0,0 +1,33 @@
+
+
+ net7.0-windows10.0.19041.0
+ 10.0.17763.0
+ Peek.FilePreviewer
+ win10-x86;win10-x64;win10-arm64
+ true
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ MSBuild:Compile
+
+
+
diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/Helpers/NativeMethods.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/Helpers/NativeMethods.cs
new file mode 100644
index 0000000000..eca34b8686
--- /dev/null
+++ b/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/Helpers/NativeMethods.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Peek.Common
+{
+ using System;
+ using System.Runtime.InteropServices;
+ using Peek.Common.Models;
+
+ public static class NativeMethods
+ {
+ [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+ internal static extern int SHCreateItemFromParsingName(
+ [MarshalAs(UnmanagedType.LPWStr)] string path,
+ IntPtr pbc,
+ ref Guid riid,
+ [MarshalAs(UnmanagedType.Interface)] out IShellItem shellItem);
+
+ [DllImport("gdi32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool DeleteObject(IntPtr hObject);
+ }
+}
diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/Helpers/ThumbnailHelper.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/Helpers/ThumbnailHelper.cs
new file mode 100644
index 0000000000..cfa2631c8d
--- /dev/null
+++ b/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/Helpers/ThumbnailHelper.cs
@@ -0,0 +1,63 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Peek.FilePreviewer.Previewers
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Runtime.InteropServices;
+ using Peek.Common;
+ using Peek.Common.Models;
+
+ public static class ThumbnailHelper
+ {
+ // Based on https://stackoverflow.com/questions/21751747/extract-thumbnail-for-any-file-in-windows
+ private const string IShellItem2Guid = "7E9FB0D3-919F-4307-AB2E-9B1860310C93";
+
+ public static readonly NativeSize HighQualityThumbnailSize = new NativeSize { Width = 720, Height = 720, };
+ public static readonly NativeSize LowQualityThumbnailSize = new NativeSize { Width = 256, Height = 256, };
+
+ private static readonly NativeSize FallBackThumbnailSize = new NativeSize { Width = 96, Height = 96, };
+ private static readonly NativeSize LastFallBackThumbnailSize = new NativeSize { Width = 32, Height = 32, };
+
+ private static readonly List ThumbnailFallBackSizes = new List
+ {
+ HighQualityThumbnailSize,
+ LowQualityThumbnailSize,
+ FallBackThumbnailSize,
+ LastFallBackThumbnailSize,
+ };
+
+ // TODO: Add a re-try system if there is no thumbnail of requested size.
+ public static HResult GetThumbnail(string filename, out IntPtr hbitmap, NativeSize thumbnailSize)
+ {
+ Guid shellItem2Guid = new Guid(IShellItem2Guid);
+ int retCode = NativeMethods.SHCreateItemFromParsingName(filename, IntPtr.Zero, ref shellItem2Guid, out IShellItem nativeShellItem);
+
+ if (retCode != 0)
+ {
+ throw Marshal.GetExceptionForHR(retCode)!;
+ }
+
+ var options = ThumbnailOptions.BiggerSizeOk | ThumbnailOptions.ThumbnailOnly | ThumbnailOptions.ScaleUp;
+
+ HResult hr = ((IShellItemImageFactory)nativeShellItem).GetImage(thumbnailSize, options, out hbitmap);
+
+ // Try to get thumbnail using the fallback sizes order
+ if (hr != HResult.Ok)
+ {
+ var currentThumbnailFallBackIndex = ThumbnailFallBackSizes.IndexOf(thumbnailSize);
+ var nextThumbnailFallBackIndex = currentThumbnailFallBackIndex + 1;
+ if (nextThumbnailFallBackIndex < ThumbnailFallBackSizes.Count - 1)
+ {
+ hr = GetThumbnail(filename, out hbitmap, ThumbnailFallBackSizes[nextThumbnailFallBackIndex]);
+ }
+ }
+
+ Marshal.ReleaseComObject(nativeShellItem);
+
+ return hr;
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/Helpers/WICHelper.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/Helpers/WICHelper.cs
new file mode 100644
index 0000000000..31af196341
--- /dev/null
+++ b/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/Helpers/WICHelper.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Peek.FilePreviewer.Previewers
+{
+ using System;
+ using System.Threading.Tasks;
+ using WIC;
+
+ public static class WICHelper
+ {
+ public static Task GetImageSize(string filePath)
+ {
+ return Task.Run(() =>
+ {
+ // TODO: Find a way to get file metadata without hydrating files. Look into Shell API/Windows Property System, e.g., IPropertyStore
+ IWICImagingFactory factory = (IWICImagingFactory)new WICImagingFactoryClass();
+ var decoder = factory.CreateDecoderFromFilename(filePath, IntPtr.Zero, StreamAccessMode.GENERIC_READ, WICDecodeOptions.WICDecodeMetadataCacheOnLoad);
+ var frame = decoder?.GetFrame(0);
+ int width = 0;
+ int height = 0;
+
+ // TODO: Respect EXIF data and find correct orientation
+ frame?.GetSize(out width, out height);
+
+ return new Windows.Foundation.Size(width, height);
+ });
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/ImagePreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/ImagePreviewer.cs
new file mode 100644
index 0000000000..05455f7ad9
--- /dev/null
+++ b/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/ImagePreviewer.cs
@@ -0,0 +1,169 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Peek.FilePreviewer.Previewers
+{
+ using System;
+ using System.Drawing.Imaging;
+ using System.IO;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using CommunityToolkit.Mvvm.ComponentModel;
+ using Microsoft.UI.Dispatching;
+ using Microsoft.UI.Xaml.Media.Imaging;
+ using Peek.Common;
+ using Windows.Foundation;
+ using File = Peek.Common.Models.File;
+
+ [INotifyPropertyChanged]
+ public partial class ImagePreviewer : IDisposable
+ {
+ [ObservableProperty]
+ private BitmapSource? preview;
+
+ public ImagePreviewer(File file)
+ {
+ File = file;
+ Dispatcher = DispatcherQueue.GetForCurrentThread();
+ }
+
+ private File File { get; }
+
+ private DispatcherQueue Dispatcher { get; }
+
+ private bool IsHighQualityThumbnailLoaded { get; set; }
+
+ private bool IsFullImageLoaded { get; set; }
+
+ private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
+
+ private CancellationToken CancellationToken => _cancellationTokenSource.Token;
+
+ public void Dispose()
+ {
+ _cancellationTokenSource.Dispose();
+ GC.SuppressFinalize(this);
+ }
+
+ public Task GetPreviewSizeAsync()
+ {
+ return WICHelper.GetImageSize(File.Path);
+ }
+
+ public Task LoadPreviewAsync()
+ {
+ var lowQualityThumbnailTask = LoadLowQualityThumbnailAsync();
+ var highQualityThumbnailTask = LoadHighQualityThumbnailAsync();
+ var fullImageTask = LoadFullQualityImageAsync();
+
+ return Task.WhenAll(lowQualityThumbnailTask, highQualityThumbnailTask, fullImageTask);
+ }
+
+ private Task LoadLowQualityThumbnailAsync()
+ {
+ var thumbnailTCS = new TaskCompletionSource();
+ Dispatcher.TryEnqueue(async () =>
+ {
+ if (CancellationToken.IsCancellationRequested)
+ {
+ _cancellationTokenSource = new CancellationTokenSource();
+ return;
+ }
+
+ if (!IsFullImageLoaded && !IsHighQualityThumbnailLoaded)
+ {
+ // TODO: Handle thumbnail errors
+ ThumbnailHelper.GetThumbnail(Path.GetFullPath(File.Path), out IntPtr hbitmap, ThumbnailHelper.LowQualityThumbnailSize);
+ var thumbnailBitmap = await GetBitmapFromHBitmapAsync(hbitmap);
+ Preview = thumbnailBitmap;
+ }
+
+ thumbnailTCS.SetResult();
+ });
+
+ return thumbnailTCS.Task;
+ }
+
+ private Task LoadHighQualityThumbnailAsync()
+ {
+ var thumbnailTCS = new TaskCompletionSource();
+ Dispatcher.TryEnqueue(async () =>
+ {
+ if (CancellationToken.IsCancellationRequested)
+ {
+ _cancellationTokenSource = new CancellationTokenSource();
+ return;
+ }
+
+ if (!IsFullImageLoaded)
+ {
+ // TODO: Handle thumbnail errors
+ ThumbnailHelper.GetThumbnail(Path.GetFullPath(File.Path), out IntPtr hbitmap, ThumbnailHelper.HighQualityThumbnailSize);
+ var thumbnailBitmap = await GetBitmapFromHBitmapAsync(hbitmap);
+ IsHighQualityThumbnailLoaded = true;
+ Preview = thumbnailBitmap;
+ }
+
+ thumbnailTCS.SetResult();
+ });
+
+ return thumbnailTCS.Task;
+ }
+
+ private Task LoadFullQualityImageAsync()
+ {
+ var fullImageTCS = new TaskCompletionSource();
+ Dispatcher.TryEnqueue(async () =>
+ {
+ // TODO: Check if this is performant
+ var bitmap = await GetFullBitmapFromPathAsync(File.Path);
+ IsFullImageLoaded = true;
+
+ if (CancellationToken.IsCancellationRequested)
+ {
+ _cancellationTokenSource = new CancellationTokenSource();
+ return;
+ }
+
+ Preview = bitmap;
+ fullImageTCS.SetResult();
+ });
+
+ return fullImageTCS.Task;
+ }
+
+ private static async Task GetFullBitmapFromPathAsync(string path)
+ {
+ var bitmap = new BitmapImage();
+ using (FileStream stream = System.IO.File.OpenRead(path))
+ {
+ await bitmap.SetSourceAsync(stream.AsRandomAccessStream());
+ }
+
+ return bitmap;
+ }
+
+ private static async Task GetBitmapFromHBitmapAsync(IntPtr hbitmap)
+ {
+ try
+ {
+ var bitmap = System.Drawing.Image.FromHbitmap(hbitmap);
+ var bitmapImage = new BitmapImage();
+ using (var stream = new MemoryStream())
+ {
+ bitmap.Save(stream, ImageFormat.Bmp);
+ stream.Position = 0;
+ await bitmapImage.SetSourceAsync(stream.AsRandomAccessStream());
+ }
+
+ return bitmapImage;
+ }
+ finally
+ {
+ // delete HBitmap to avoid memory leaks
+ NativeMethods.DeleteObject(hbitmap);
+ }
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.UI.WPF/App.xaml b/src/modules/peek/Peek.UI.WPF/App.xaml
new file mode 100644
index 0000000000..1fc9407448
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/App.xaml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/peek/Peek.UI.WPF/App.xaml.cs b/src/modules/peek/Peek.UI.WPF/App.xaml.cs
new file mode 100644
index 0000000000..54225caef6
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/App.xaml.cs
@@ -0,0 +1,94 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading;
+using System.Windows;
+using Common.UI;
+using ManagedCommon;
+
+namespace Peek.UI
+{
+ ///
+ /// Interaction logic for App.xaml
+ ///
+ public partial class App : Application, IDisposable
+ {
+ private ThemeManager? _themeManager;
+
+ private Mutex? _instanceMutex;
+ private static string[] _args = Array.Empty();
+ private int _powerToysRunnerPid;
+ private bool disposedValue;
+
+ // TODO: Make sure no window appears or blinks at startup
+ protected override void OnStartup(StartupEventArgs e)
+ {
+ _args = e?.Args ?? Array.Empty();
+
+ // allow only one instance of peek
+ _instanceMutex = new Mutex(true, @"Local\PowerToys_Peek_InstanceMutex", out bool createdNew);
+ if (!createdNew)
+ {
+ _instanceMutex = null;
+ Environment.Exit(0);
+ return;
+ }
+
+ if (_args?.Length > 0)
+ {
+ _ = int.TryParse(_args[0], out _powerToysRunnerPid);
+
+ RunnerHelper.WaitForPowerToysRunner(_powerToysRunnerPid, () =>
+ {
+ Environment.Exit(0);
+ });
+ }
+ else
+ {
+ _powerToysRunnerPid = -1;
+ }
+
+ _themeManager = new ThemeManager(this);
+ base.OnStartup(e);
+ }
+
+ protected override void OnExit(ExitEventArgs e)
+ {
+ if (_instanceMutex != null)
+ {
+ _instanceMutex.ReleaseMutex();
+ }
+
+ base.OnExit(e);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposedValue)
+ {
+ if (disposing)
+ {
+ _instanceMutex?.Dispose();
+ }
+
+ // TODO: free unmanaged resources (unmanaged objects) and override finalizer
+ // TODO: set large fields to null
+ disposedValue = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ public bool IsRunningDetachedFromPowerToys()
+ {
+ return _powerToysRunnerPid == -1;
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.UI.WPF/AssemblyInfo.cs b/src/modules/peek/Peek.UI.WPF/AssemblyInfo.cs
new file mode 100644
index 0000000000..bcac370d7d
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/AssemblyInfo.cs
@@ -0,0 +1,10 @@
+// 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.Windows;
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, // where theme specific resource dictionaries are located (used if a resource is not found in the page, or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly) // where the generic resource dictionary is locate (used if a resource is not found in the page, app, or any theme specific resource dictionaries)
+]
diff --git a/src/modules/peek/Peek.UI.WPF/Assets/error.png b/src/modules/peek/Peek.UI.WPF/Assets/error.png
new file mode 100644
index 0000000000..c2eb2fc2df
Binary files /dev/null and b/src/modules/peek/Peek.UI.WPF/Assets/error.png differ
diff --git a/src/modules/peek/Peek.UI.WPF/Extensions/LinkedListNodeExtensions.cs b/src/modules/peek/Peek.UI.WPF/Extensions/LinkedListNodeExtensions.cs
new file mode 100644
index 0000000000..5e740726e4
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/Extensions/LinkedListNodeExtensions.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+
+namespace Peek.UI.Extensions
+{
+ public static class LinkedListNodeExtensions
+ {
+ public static LinkedListNode? GetNextOrFirst(this LinkedListNode current)
+ {
+ return current.Next ?? current.List?.First;
+ }
+
+ public static LinkedListNode? GetPreviousOrLast(this LinkedListNode current)
+ {
+ return current.Previous ?? current.List?.Last;
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.UI.WPF/Extensions/SizeExtensions.cs b/src/modules/peek/Peek.UI.WPF/Extensions/SizeExtensions.cs
new file mode 100644
index 0000000000..19ac26c8b9
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/Extensions/SizeExtensions.cs
@@ -0,0 +1,61 @@
+// 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.Windows;
+
+namespace Peek.UI.Extensions
+{
+ public static class SizeExtensions
+ {
+ public static Rect Fit(this Size sizeToFit, Rect bounds, Size maxSize, Size minSize, Size allowedGap, double reservedHeight)
+ {
+ double resultingWidth = sizeToFit.Width;
+ double resultingHeight = sizeToFit.Height;
+
+ var ratioWidth = sizeToFit.Width / maxSize.Width;
+ var ratioHeight = sizeToFit.Height / maxSize.Height;
+
+ if (ratioWidth > ratioHeight)
+ {
+ if (ratioWidth > 1)
+ {
+ resultingWidth = maxSize.Width;
+ resultingHeight = sizeToFit.Height / ratioWidth;
+ }
+ }
+ else
+ {
+ if (ratioHeight > 1)
+ {
+ resultingWidth = sizeToFit.Width / ratioHeight;
+ resultingHeight = maxSize.Height;
+ }
+ }
+
+ if (resultingWidth < minSize.Width - allowedGap.Width)
+ {
+ resultingWidth = minSize.Width;
+ }
+
+ if (resultingHeight < minSize.Height - allowedGap.Height)
+ {
+ resultingHeight = minSize.Height;
+ }
+
+ resultingHeight += reservedHeight;
+
+ // Calculate offsets to center content
+ double offsetX = (maxSize.Width - resultingWidth) / 2;
+ double offsetY = (maxSize.Height - resultingHeight) / 2;
+
+ var maxWindowLeft = bounds.Left + ((bounds.Right - bounds.Left - maxSize.Width) / 2);
+ var maxWindowTop = bounds.Top + ((bounds.Bottom - bounds.Top - maxSize.Height) / 2);
+
+ var resultingLeft = maxWindowLeft + offsetX;
+ var resultingTop = maxWindowTop + offsetY;
+
+ return new Rect(resultingLeft, resultingTop, resultingWidth, resultingHeight);
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.UI.WPF/Extensions/WindowExtensions.cs b/src/modules/peek/Peek.UI.WPF/Extensions/WindowExtensions.cs
new file mode 100644
index 0000000000..17abcda12c
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/Extensions/WindowExtensions.cs
@@ -0,0 +1,41 @@
+// 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.Windows;
+using System.Windows.Interop;
+using Peek.UI.Native;
+using static Peek.UI.Native.NativeModels;
+
+namespace Peek.UI.Extensions
+{
+ public static class WindowExtensions
+ {
+ public static void SetToolStyle(this Window window)
+ {
+ var handle = new WindowInteropHelper(window).Handle;
+ _ = NativeMethods.SetWindowLong(handle, GwlExStyle, NativeMethods.GetWindowLong(handle, GwlExStyle) | WsExToolWindow);
+ }
+
+ public static void BringToForeground(this Window window)
+ {
+ // Use SendInput hack to allow Activate to work - required to resolve focus issue https://github.com/microsoft/PowerToys/issues/4270
+ Input input = new Input { Type = InputType.InputMouse, Data = { } };
+ Input[] inputs = new Input[] { input };
+
+ // Send empty mouse event. This makes this thread the last to send input, and hence allows it to pass foreground permission checks
+ _ = NativeMethods.SendInput(1, inputs, Input.Size);
+
+ window.Activate();
+ }
+
+ public static void RoundCorners(this Window window)
+ {
+ IntPtr hWnd = new System.Windows.Interop.WindowInteropHelper(Window.GetWindow(window)).EnsureHandle();
+ var attribute = DwmWindowAttributed.DwmaWindowCornerPreference;
+ var preference = DwmWindowCornerPreference.DwmCpRound;
+ NativeMethods.DwmSetWindowAttribute(hWnd, attribute, ref preference, sizeof(uint));
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.UI.WPF/Helpers/FileExplorerHelper.cs b/src/modules/peek/Peek.UI.WPF/Helpers/FileExplorerHelper.cs
new file mode 100644
index 0000000000..c5262a3279
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/Helpers/FileExplorerHelper.cs
@@ -0,0 +1,34 @@
+// 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;
+
+namespace Peek.UI.Helpers
+{
+ public static class FileExplorerHelper
+ {
+ public static IEnumerable GetSelectedItems(IntPtr handle)
+ {
+ var selectedItems = new List();
+ var shell = new Shell32.Shell();
+ foreach (SHDocVw.InternetExplorer window in shell.Windows())
+ {
+ if (window.HWND == (int)handle)
+ {
+ Shell32.FolderItems items = ((Shell32.IShellFolderViewDual2)window.Document).SelectedItems();
+ if (items != null && items.Count > 0)
+ {
+ foreach (Shell32.FolderItem item in items)
+ {
+ selectedItems.Add(item.Path);
+ }
+ }
+ }
+ }
+
+ return selectedItems;
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.UI.WPF/Helpers/FileLoadHelper.cs b/src/modules/peek/Peek.UI.WPF/Helpers/FileLoadHelper.cs
new file mode 100644
index 0000000000..e43e7d72c2
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/Helpers/FileLoadHelper.cs
@@ -0,0 +1,122 @@
+// 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.Drawing.Imaging;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Media.Imaging;
+using Peek.UI.Models;
+
+namespace Peek.UI.Helpers
+{
+ public static class FileLoadHelper
+ {
+ public static Task LoadDimensionsAsync(string filename)
+ {
+ return Task.Run(() =>
+ {
+ Size size = new Size(0, 0);
+ try
+ {
+ using (FileStream stream = File.OpenRead(filename))
+ {
+ string extension = Path.GetExtension(stream.Name);
+ if (FileTypeHelper.IsSupportedImage(extension))
+ {
+ using (System.Drawing.Image sourceImage = System.Drawing.Image.FromStream(stream, false, false))
+ {
+ var rotation = EvaluateRotationToApply(sourceImage);
+ if (rotation == Rotation.Rotate90 || rotation == Rotation.Rotate270)
+ {
+ size = new Size(sourceImage.Height, sourceImage.Width);
+ }
+ else
+ {
+ size = new Size(sourceImage.Width, sourceImage.Height);
+ }
+
+ return Task.FromResult(new DimensionData { Size = size, Rotation = rotation });
+ }
+ }
+ else
+ {
+ return Task.FromResult(new DimensionData { Size = size, Rotation = Rotation.Rotate0 });
+ }
+ }
+ }
+ catch (Exception)
+ {
+ return Task.FromResult(new DimensionData { Size = size, Rotation = Rotation.Rotate0 });
+ }
+ });
+ }
+
+ public static async Task LoadThumbnailAsync(string filename, bool iconFallback)
+ {
+ var thumbnail = await Task.Run(() =>
+ {
+ var bitmapSource = ThumbnailHelper.GetThumbnail(filename, iconFallback);
+ bitmapSource.Freeze();
+ return bitmapSource;
+ });
+
+ return thumbnail;
+ }
+
+ public static Task LoadIconAsync(string filename)
+ {
+ return Task.Run(() =>
+ {
+ var bitmapSource = ThumbnailHelper.GetIcon(filename);
+ bitmapSource.Freeze();
+ return bitmapSource;
+ });
+ }
+
+ public static Task LoadFullImageAsync(string filename, Rotation rotation)
+ {
+ return Task.Run(() =>
+ {
+ var bitmap = new BitmapImage();
+ bitmap.BeginInit();
+ bitmap.CacheOption = BitmapCacheOption.OnLoad;
+ bitmap.UriSource = new Uri(filename);
+ bitmap.Rotation = rotation;
+ bitmap.EndInit();
+ bitmap.Freeze();
+ return bitmap;
+ });
+ }
+
+ private static Rotation EvaluateRotationToApply(System.Drawing.Image image)
+ {
+ PropertyItem? property = image.PropertyItems?.FirstOrDefault(p => p.Id == 274);
+
+ if (property != null && property.Value != null && property.Value.Length > 0)
+ {
+ int orientation = property.Value[0];
+
+ if (orientation == 6)
+ {
+ return Rotation.Rotate90;
+ }
+
+ if (orientation == 3)
+ {
+ return Rotation.Rotate180;
+ }
+
+ if (orientation == 8)
+ {
+ return Rotation.Rotate270;
+ }
+ }
+
+ return Rotation.Rotate0;
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.UI.WPF/Helpers/FileTypeHelper.cs b/src/modules/peek/Peek.UI.WPF/Helpers/FileTypeHelper.cs
new file mode 100644
index 0000000000..40b6a2ad23
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/Helpers/FileTypeHelper.cs
@@ -0,0 +1,77 @@
+// 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 Peek.UI.Native;
+using static Peek.UI.Native.NativeModels;
+
+namespace Peek.UI.Helpers
+{
+ public static class FileTypeHelper
+ {
+ public static bool IsSupportedImage(string extension) => extension switch
+ {
+ ".bmp" => true,
+ ".gif" => true,
+ ".jpg" => true,
+ ".jfif" => true,
+ ".jfi" => true,
+ ".jif" => true,
+ ".jpeg" => true,
+ ".jpe" => true,
+ ".png" => true,
+ ".tif" => true,
+ ".tiff" => true,
+ _ => false,
+ };
+
+ public static bool IsMedia(string extension)
+ {
+ return IsImage(extension) || IsVideo(extension);
+ }
+
+ public static bool IsImage(string extension)
+ {
+ return IsPerceivedType(extension, PerceivedType.Image);
+ }
+
+ public static bool IsVideo(string extension)
+ {
+ return IsPerceivedType(extension, PerceivedType.Video);
+ }
+
+ public static bool IsDocument(string extension)
+ {
+ return IsPerceivedType(extension, PerceivedType.Document);
+ }
+
+ internal static bool IsPerceivedType(string extension, PerceivedType perceivedType)
+ {
+ if (string.IsNullOrEmpty(extension))
+ {
+ return false;
+ }
+
+ PerceivedType perceived;
+ Perceived flag;
+ bool isPerceivedType = false;
+
+ try
+ {
+ if (NativeMethods.AssocGetPerceivedType(extension, out perceived, out flag, IntPtr.Zero) == HResult.Ok)
+ {
+ isPerceivedType = perceived == perceivedType;
+ }
+ }
+ catch (Exception)
+ {
+ // TODO: AssocGetPerceivedType throws on some file types (json, ps1, exe, etc.)
+ // Properly handle these
+ return false;
+ }
+
+ return isPerceivedType;
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.UI.WPF/Helpers/ThumbnailHelper.cs b/src/modules/peek/Peek.UI.WPF/Helpers/ThumbnailHelper.cs
new file mode 100644
index 0000000000..77a9bcf099
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/Helpers/ThumbnailHelper.cs
@@ -0,0 +1,128 @@
+// 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.IO;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Windows;
+using System.Windows.Interop;
+using System.Windows.Media.Imaging;
+using Peek.UI.Models;
+using Peek.UI.Native;
+using static Peek.UI.Native.NativeModels;
+
+namespace Peek.UI.Helpers
+{
+ public static class ThumbnailHelper
+ {
+ private static readonly Assembly Assembly = Assembly.GetExecutingAssembly();
+ public static readonly string ProgramDirectory = Directory.GetParent(Assembly.Location)!.ToString();
+ public static readonly string ErrorIcon = Path.Combine(ProgramDirectory, "Assets", "error.png");
+
+ // Based on https://stackoverflow.com/questions/21751747/extract-thumbnail-for-any-file-in-windows
+ private const string IShellItem2Guid = "7E9FB0D3-919F-4307-AB2E-9B1860310C93";
+
+ public static BitmapSource GetIcon(string fileName)
+ {
+ IntPtr hbitmap;
+ HResult hr = GetIconImpl(Path.GetFullPath(fileName), out hbitmap);
+
+ if (hr != HResult.Ok)
+ {
+ return new BitmapImage(new Uri(ErrorIcon));
+ }
+
+ try
+ {
+ return Imaging.CreateBitmapSourceFromHBitmap(hbitmap, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
+ }
+ finally
+ {
+ // delete HBitmap to avoid memory leaks
+ NativeMethods.DeleteObject(hbitmap);
+ }
+ }
+
+ public static BitmapSource GetThumbnail(string fileName, bool iconFallback)
+ {
+ IntPtr hbitmap;
+ HResult hr = GetThumbnailImpl(Path.GetFullPath(fileName), out hbitmap);
+
+ if (hr != HResult.Ok && iconFallback)
+ {
+ return GetIcon(fileName);
+ }
+
+ try
+ {
+ return Imaging.CreateBitmapSourceFromHBitmap(hbitmap, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
+ }
+ finally
+ {
+ // delete HBitmap to avoid memory leaks
+ NativeMethods.DeleteObject(hbitmap);
+ }
+ }
+
+ private static HResult GetIconImpl(string filename, out IntPtr hbitmap)
+ {
+ Guid shellItem2Guid = new Guid(IShellItem2Guid);
+ int retCode = NativeMethods.SHCreateItemFromParsingName(filename, IntPtr.Zero, ref shellItem2Guid, out IShellItem nativeShellItem);
+
+ if (retCode != 0)
+ {
+ throw Marshal.GetExceptionForHR(retCode)!;
+ }
+
+ NativeSize large = new NativeSize { Width = 256, Height = 256 };
+ var options = ThumbnailOptions.BiggerSizeOk | ThumbnailOptions.IconOnly;
+
+ HResult hr = ((IShellItemImageFactory)nativeShellItem).GetImage(large, options, out hbitmap);
+
+ Marshal.ReleaseComObject(nativeShellItem);
+
+ return hr;
+ }
+
+ private static HResult GetThumbnailImpl(string filename, out IntPtr hbitmap)
+ {
+ Guid shellItem2Guid = new Guid(IShellItem2Guid);
+ int retCode = NativeMethods.SHCreateItemFromParsingName(filename, IntPtr.Zero, ref shellItem2Guid, out IShellItem nativeShellItem);
+
+ if (retCode != 0)
+ {
+ throw Marshal.GetExceptionForHR(retCode)!;
+ }
+
+ var extraLarge = new NativeSize { Width = 1024, Height = 1024, };
+ var large = new NativeSize { Width = 256, Height = 256 };
+ var medium = new NativeSize { Width = 96, Height = 96 };
+ var small = new NativeSize { Width = 32, Height = 32 };
+
+ var options = ThumbnailOptions.BiggerSizeOk | ThumbnailOptions.ThumbnailOnly | ThumbnailOptions.ScaleUp;
+
+ HResult hr = ((IShellItemImageFactory)nativeShellItem).GetImage(extraLarge, options, out hbitmap);
+
+ if (hr != HResult.Ok)
+ {
+ hr = ((IShellItemImageFactory)nativeShellItem).GetImage(large, options, out hbitmap);
+ }
+
+ if (hr != HResult.Ok)
+ {
+ hr = ((IShellItemImageFactory)nativeShellItem).GetImage(medium, options, out hbitmap);
+ }
+
+ if (hr != HResult.Ok)
+ {
+ hr = ((IShellItemImageFactory)nativeShellItem).GetImage(small, options, out hbitmap);
+ }
+
+ Marshal.ReleaseComObject(nativeShellItem);
+
+ return hr;
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.UI.WPF/Models/DimensionData.cs b/src/modules/peek/Peek.UI.WPF/Models/DimensionData.cs
new file mode 100644
index 0000000000..42d0567238
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/Models/DimensionData.cs
@@ -0,0 +1,21 @@
+// 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.Drawing.Imaging;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Media.Imaging;
+
+namespace Peek.UI.Models
+{
+ public class DimensionData
+ {
+ public Size Size { get; set; }
+
+ public Rotation Rotation { get; set; }
+ }
+}
diff --git a/src/modules/peek/Peek.UI.WPF/Models/IShellItem.cs b/src/modules/peek/Peek.UI.WPF/Models/IShellItem.cs
new file mode 100644
index 0000000000..908e79428c
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/Models/IShellItem.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Runtime.InteropServices;
+using static Peek.UI.Native.NativeModels;
+
+namespace Peek.UI.Models
+{
+ [ComImport]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ [Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe")]
+ internal interface IShellItem
+ {
+ void BindToHandler(
+ IntPtr pbc,
+ [MarshalAs(UnmanagedType.LPStruct)] Guid bhid,
+ [MarshalAs(UnmanagedType.LPStruct)] Guid riid,
+ out IntPtr ppv);
+
+ void GetParent(out IShellItem ppsi);
+
+ void GetDisplayName(Sigdn sigdnName, out IntPtr ppszName);
+
+ void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
+
+ void Compare(IShellItem psi, uint hint, out int piOrder);
+ }
+}
diff --git a/src/modules/peek/Peek.UI.WPF/Models/ObservableObject.cs b/src/modules/peek/Peek.UI.WPF/Models/ObservableObject.cs
new file mode 100644
index 0000000000..8925029cbd
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/Models/ObservableObject.cs
@@ -0,0 +1,19 @@
+// 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.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace Peek.UI.Models
+{
+ public abstract class ObservableObject : INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.UI.WPF/Models/ObservableRectangle.cs b/src/modules/peek/Peek.UI.WPF/Models/ObservableRectangle.cs
new file mode 100644
index 0000000000..3a9c91c523
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/Models/ObservableRectangle.cs
@@ -0,0 +1,85 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Peek.UI.Models
+{
+ public class ObservableRectangle : ObservableObject
+ {
+ private double _left;
+
+ public double Left
+ {
+ get
+ {
+ return _left;
+ }
+
+ set
+ {
+ if (_left != value)
+ {
+ _left = value;
+ OnPropertyChanged(nameof(Left));
+ }
+ }
+ }
+
+ private double _top;
+
+ public double Top
+ {
+ get
+ {
+ return _top;
+ }
+
+ set
+ {
+ if (_top != value)
+ {
+ _top = value;
+ OnPropertyChanged(nameof(Top));
+ }
+ }
+ }
+
+ private double _height;
+
+ public double Height
+ {
+ get
+ {
+ return _height;
+ }
+
+ set
+ {
+ if (_height != value)
+ {
+ _height = value;
+ OnPropertyChanged(nameof(Height));
+ }
+ }
+ }
+
+ private double _width;
+
+ public double Width
+ {
+ get
+ {
+ return _width;
+ }
+
+ set
+ {
+ if (_width != value)
+ {
+ _width = value;
+ OnPropertyChanged(nameof(Width));
+ }
+ }
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.UI.WPF/Models/ObservableWindowData.cs b/src/modules/peek/Peek.UI.WPF/Models/ObservableWindowData.cs
new file mode 100644
index 0000000000..0aab543369
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/Models/ObservableWindowData.cs
@@ -0,0 +1,88 @@
+// 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.Windows;
+
+namespace Peek.UI.Models
+{
+ public class ObservableWindowData : ObservableObject
+ {
+ private double _titleBarHeight;
+
+ public double TitleBarHeight
+ {
+ get
+ {
+ return _titleBarHeight;
+ }
+
+ set
+ {
+ if (_titleBarHeight != value)
+ {
+ _titleBarHeight = value;
+ OnPropertyChanged(nameof(TitleBarHeight));
+ }
+ }
+ }
+
+ private ObservableRectangle _rectangle = new ObservableRectangle();
+
+ public ObservableRectangle Rectangle
+ {
+ get
+ {
+ return _rectangle;
+ }
+
+ set
+ {
+ if (_rectangle != value)
+ {
+ _rectangle = value;
+ OnPropertyChanged(nameof(Rectangle));
+ }
+ }
+ }
+
+ private string _title = string.Empty;
+
+ public string Title
+ {
+ get
+ {
+ return _title;
+ }
+
+ set
+ {
+ if (_title != value)
+ {
+ _title = value;
+ OnPropertyChanged(nameof(Title));
+ }
+ }
+ }
+
+ private Visibility _visibility;
+
+ public Visibility Visibility
+ {
+ get
+ {
+ return _visibility;
+ }
+
+ set
+ {
+ if (_visibility != value)
+ {
+ _visibility = value;
+
+ OnPropertyChanged(nameof(Visibility));
+ }
+ }
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.UI.WPF/Native/NativeEventWaiter.cs b/src/modules/peek/Peek.UI.WPF/Native/NativeEventWaiter.cs
new file mode 100644
index 0000000000..efb07bf413
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/Native/NativeEventWaiter.cs
@@ -0,0 +1,28 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading;
+using System.Windows;
+
+namespace Peek.UI.Native
+{
+ public static class NativeEventWaiter
+ {
+ public static void WaitForEventLoop(string eventName, Action callback)
+ {
+ new Thread(() =>
+ {
+ var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, eventName);
+ while (true)
+ {
+ if (eventHandle.WaitOne())
+ {
+ Application.Current.Dispatcher.Invoke(callback);
+ }
+ }
+ }).Start();
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.UI.WPF/Native/NativeMethods.cs b/src/modules/peek/Peek.UI.WPF/Native/NativeMethods.cs
new file mode 100644
index 0000000000..2be17a45d1
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/Native/NativeMethods.cs
@@ -0,0 +1,54 @@
+// 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.Runtime.InteropServices;
+using Peek.UI.Models;
+using static Peek.UI.Native.NativeModels;
+
+namespace Peek.UI.Native
+{
+ public static class NativeMethods
+ {
+ [DllImport("dwmapi.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+ internal static extern long DwmSetWindowAttribute(
+ IntPtr hwnd,
+ DwmWindowAttributed attribute,
+ ref DwmWindowCornerPreference pvAttribute,
+ uint cbAttribute);
+
+ [DllImport("gdi32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool DeleteObject(IntPtr hObject);
+
+ [DllImport("Shlwapi.dll", ExactSpelling = true, PreserveSig = false)]
+ internal static extern HResult AssocGetPerceivedType(
+ [MarshalAs(UnmanagedType.LPWStr)] string extension,
+ out PerceivedType perceivedType,
+ out Perceived perceivedFlags,
+ IntPtr ptrType);
+
+ [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+ internal static extern int SHCreateItemFromParsingName(
+ [MarshalAs(UnmanagedType.LPWStr)] string path,
+ IntPtr pbc,
+ ref Guid riid,
+ [MarshalAs(UnmanagedType.Interface)] out IShellItem shellItem);
+
+ [DllImport("user32.dll", SetLastError = true)]
+ internal static extern int GetWindowLong(IntPtr hWnd, int nIndex);
+
+ [DllImport("user32.dll")]
+ internal static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
+
+ [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
+ internal static extern IntPtr GetForegroundWindow();
+
+ [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
+ internal static extern int GetWindowThreadProcessId(IntPtr handle, out int processId);
+
+ [DllImport("user32.dll")]
+ internal static extern uint SendInput(uint nInputs, Input[] pInputs, int cbSize);
+ }
+}
diff --git a/src/modules/peek/Peek.UI.WPF/Native/NativeModels.cs b/src/modules/peek/Peek.UI.WPF/Native/NativeModels.cs
new file mode 100644
index 0000000000..1cb3b6421e
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/Native/NativeModels.cs
@@ -0,0 +1,182 @@
+// 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.Runtime.InteropServices;
+
+namespace Peek.UI.Native
+{
+ public class NativeModels
+ {
+ public const int GwlExStyle = -20;
+ public const int WsExToolWindow = 0x00000080;
+
+ public enum PerceivedType
+ {
+ Folder = -1,
+ Unknown = 0,
+ Image = 2,
+ Video = 4,
+ Document = 6,
+ }
+
+ public enum Perceived
+ {
+ Undefined = 0x0000,
+ Softcoded = 0x0001,
+ Hardcoded = 0x0002,
+ NativeSupport = 0x0004,
+ GdiPlus = 0x0010,
+ WMSDK = 0x0020,
+ ZipFolder = 0x0040,
+ }
+
+ public enum HResult
+ {
+ Ok = 0x0000,
+ False = 0x0001,
+ InvalidArguments = unchecked((int)0x80070057),
+ OutOfMemory = unchecked((int)0x8007000E),
+ NoInterface = unchecked((int)0x80004002),
+ Fail = unchecked((int)0x80004005),
+ ExtractionFailed = unchecked((int)0x8004B200),
+ ElementNotFound = unchecked((int)0x80070490),
+ TypeElementNotFound = unchecked((int)0x8002802B),
+ NoObject = unchecked((int)0x800401E5),
+ Win32ErrorCanceled = 1223,
+ Canceled = unchecked((int)0x800704C7),
+ ResourceInUse = unchecked((int)0x800700AA),
+ AccessDenied = unchecked((int)0x80030005),
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct Input
+ {
+ public InputType Type;
+ public InputUnion Data;
+
+ public static int Size
+ {
+ get { return Marshal.SizeOf(typeof(Input)); }
+ }
+ }
+
+ [StructLayout(LayoutKind.Explicit)]
+ public struct InputUnion
+ {
+ [FieldOffset(0)]
+ public MouseInput Mi;
+
+ [FieldOffset(0)]
+ public KeybdInput Ki;
+
+ [FieldOffset(0)]
+ public HardwareInput Hi;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct MouseInput
+ {
+ public int Dx;
+ public int Dy;
+ public int MouseData;
+ public uint DwFlags;
+ public uint Time;
+ public UIntPtr DwExtraInfo;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct KeybdInput
+ {
+ public short WVk;
+ public short WScan;
+ public uint DwFlags;
+ public int Time;
+ public UIntPtr DwExtraInfo;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct HardwareInput
+ {
+ public int UMsg;
+ public short WParamL;
+ public short WParamH;
+ }
+
+ public enum InputType : uint
+ {
+ InputMouse = 0,
+ InputKeyboard = 1,
+ InputHardware = 2,
+ }
+
+ public enum Sigdn : uint
+ {
+ NormalDisplay = 0,
+ ParentRelativeParsing = 0x80018001,
+ ParentRelativeForAddressBar = 0x8001c001,
+ DesktopAbsoluteParsing = 0x80028000,
+ ParentRelativeEditing = 0x80031001,
+ DesktopAbsoluteEditing = 0x8004c000,
+ FileSysPath = 0x80058000,
+ Url = 0x80068000,
+ }
+
+ public enum DwmWindowAttributed
+ {
+ DwmaWindowCornerPreference = 33,
+ }
+
+ // The DWM_WINDOW_CORNER_PREFERENCE enum for DwmSetWindowAttribute's third parameter, which tells the function
+ // what value of the enum to set.
+ public enum DwmWindowCornerPreference
+ {
+ DwmCpDefault = 0,
+ DwmCpDoNotRound = 1,
+ DwmCpRound = 2,
+ DwmCpRoundSmall = 3,
+ }
+
+ [Flags]
+ public enum ThumbnailOptions
+ {
+ None = 0x00,
+ BiggerSizeOk = 0x01,
+ InMemoryOnly = 0x02,
+ IconOnly = 0x04,
+ ThumbnailOnly = 0x08,
+ InCacheOnly = 0x10,
+ ScaleUp = 0x100,
+ }
+
+ [ComImport]
+ [Guid("bcc18b79-ba16-442f-80c4-8a59c30c463b")]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ internal interface IShellItemImageFactory
+ {
+ [PreserveSig]
+ HResult GetImage(
+ [In, MarshalAs(UnmanagedType.Struct)] NativeSize size,
+ [In] ThumbnailOptions flags,
+ [Out] out IntPtr phbm);
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct NativeSize
+ {
+ private int width;
+ private int height;
+
+ public int Width
+ {
+ set { width = value; }
+ }
+
+ public int Height
+ {
+ set { height = value; }
+ }
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.UI.WPF/Peek.UI.WPF.csproj b/src/modules/peek/Peek.UI.WPF/Peek.UI.WPF.csproj
new file mode 100644
index 0000000000..42d2f14957
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/Peek.UI.WPF.csproj
@@ -0,0 +1,90 @@
+
+
+
+ WinExe
+ net7.0-windows10.0.19041.0
+ PowerToys.Peek.UI
+ enable
+ true
+ false
+ false
+ ..\..\..\..\$(Platform)\$(Configuration)\modules\Peek\
+ True
+ Resources\FluentIconsPeek.ico
+
+
+
+
+ tlbimp
+ 0
+ 1
+ 50a7e9b0-70ef-11d1-b75a-00a0c90564fe
+ 0
+ false
+ true
+
+
+ tlbimp
+ 1
+ 1
+ eab22ac0-30c1-11cf-a7eb-0000c05bae0b
+ 0
+ false
+ true
+
+
+
+
+ Resources\Peek.ico
+
+
+
+
+
+
+
+
+ Always
+
+
+ Always
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $(DefaultXamlRuntime)
+
+
+ $(DefaultXamlRuntime)
+
+
+ $(DefaultXamlRuntime)
+
+
+ $(DefaultXamlRuntime)
+
+
+ $(DefaultXamlRuntime)
+
+
+ $(DefaultXamlRuntime)
+
+
+
+
+
+
+
+
diff --git a/src/modules/peek/Peek.UI.WPF/Resources/Peek.ico b/src/modules/peek/Peek.UI.WPF/Resources/Peek.ico
new file mode 100644
index 0000000000..daf81fdeb3
Binary files /dev/null and b/src/modules/peek/Peek.UI.WPF/Resources/Peek.ico differ
diff --git a/src/modules/peek/Peek.UI.WPF/Themes/Dark.xaml b/src/modules/peek/Peek.UI.WPF/Themes/Dark.xaml
new file mode 100644
index 0000000000..14204cfe7f
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/Themes/Dark.xaml
@@ -0,0 +1,19 @@
+
+
+
+ Dark.Accent1
+ PowerToysPeek
+ Accent1 (Dark)
+ Dark
+ Accent1
+ Black
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/peek/Peek.UI.WPF/Themes/HighContrast1.xaml b/src/modules/peek/Peek.UI.WPF/Themes/HighContrast1.xaml
new file mode 100644
index 0000000000..cf168baaae
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/Themes/HighContrast1.xaml
@@ -0,0 +1,19 @@
+
+
+
+ HighContrast.Accent2
+ PowerToysPeek
+ Accent2 (HighContrast)
+ HighContrast
+ Accent2
+ White
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/peek/Peek.UI.WPF/Themes/HighContrast2.xaml b/src/modules/peek/Peek.UI.WPF/Themes/HighContrast2.xaml
new file mode 100644
index 0000000000..65d8065e29
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/Themes/HighContrast2.xaml
@@ -0,0 +1,19 @@
+
+
+
+ HighContrast.Accent3
+ PowerToysPeek
+ Accent3 (HighContrast)
+ HighContrast
+ Accent3
+ White
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/peek/Peek.UI.WPF/Themes/HighContrastBlack.xaml b/src/modules/peek/Peek.UI.WPF/Themes/HighContrastBlack.xaml
new file mode 100644
index 0000000000..402f414db6
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/Themes/HighContrastBlack.xaml
@@ -0,0 +1,19 @@
+
+
+
+ HighContrast.Accent4
+ PowerToysPeek
+ Accent4 (HighContrast)
+ HighContrast
+ Accent4
+ White
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/peek/Peek.UI.WPF/Themes/HighContrastWhite.xaml b/src/modules/peek/Peek.UI.WPF/Themes/HighContrastWhite.xaml
new file mode 100644
index 0000000000..bfe85de54f
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/Themes/HighContrastWhite.xaml
@@ -0,0 +1,19 @@
+
+
+
+ HighContrast.Accent5
+ PowerToysPeek
+ Accent5 (HighContrast)
+ HighContrast
+ Accent5
+ White
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/peek/Peek.UI.WPF/Themes/Light.xaml b/src/modules/peek/Peek.UI.WPF/Themes/Light.xaml
new file mode 100644
index 0000000000..4ca102d2fe
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/Themes/Light.xaml
@@ -0,0 +1,19 @@
+
+
+
+ Light.Accent1
+ PowerToysPeek
+ Accent1 (Light)
+ Light
+ Accent1
+ White
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/peek/Peek.UI.WPF/ViewModels/MainViewModel.cs b/src/modules/peek/Peek.UI.WPF/ViewModels/MainViewModel.cs
new file mode 100644
index 0000000000..406a36b4a3
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/ViewModels/MainViewModel.cs
@@ -0,0 +1,301 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media.Imaging;
+using Peek.UI.Extensions;
+using Peek.UI.Helpers;
+using Peek.UI.Models;
+using Peek.UI.Native;
+using WpfScreenHelper;
+using Size = System.Windows.Size;
+
+namespace Peek.UI.ViewModels
+{
+ public class MainViewModel : ObservableObject, IDisposable
+ {
+ private const double ImageScale = 0.75;
+ private static readonly Size MinWindowSize = new Size(720, 720);
+ private static readonly Size AllowedContentGap = new Size(220, 220);
+
+ private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
+
+ private CancellationToken CancellationToken => _cancellationTokenSource.Token;
+
+ public IntPtr ForegroundWindowHandle { get; internal set; }
+
+ public Image ImageControl { get; set; }
+
+ public LinkedList SelectedFilePaths { get; set; } = new LinkedList();
+
+ private BitmapSource? _bitmap;
+
+ public BitmapSource? Bitmap
+ {
+ get
+ {
+ return _bitmap;
+ }
+
+ set
+ {
+ if (_bitmap != value)
+ {
+ _bitmap = value;
+ OnPropertyChanged(nameof(Bitmap));
+ }
+ }
+ }
+
+ private LinkedListNode? _currentSelectedFilePath;
+
+ public LinkedListNode? CurrentSelectedFilePath
+ {
+ get
+ {
+ return _currentSelectedFilePath;
+ }
+
+ set
+ {
+ if (_currentSelectedFilePath != value)
+ {
+ _currentSelectedFilePath = value;
+ var title = Path.GetFileName(_currentSelectedFilePath?.Value ?? string.Empty);
+ MainWindowData.Title = title;
+ OnPropertyChanged(nameof(CurrentSelectedFilePath));
+ }
+ }
+ }
+
+ public Visibility IsImageReady => IsLoading ? Visibility.Collapsed : Visibility.Visible;
+
+ private bool _isLoading = true;
+
+ public bool IsLoading
+ {
+ get
+ {
+ return _isLoading;
+ }
+
+ set
+ {
+ if (_isLoading != value)
+ {
+ _isLoading = value;
+ OnPropertyChanged(nameof(IsLoading));
+ OnPropertyChanged(nameof(IsImageReady));
+ }
+ }
+ }
+
+ private ObservableWindowData _mainWindowData = new ObservableWindowData();
+
+ public ObservableWindowData MainWindowData
+ {
+ get
+ {
+ return _mainWindowData;
+ }
+
+ set
+ {
+ if (_mainWindowData != value)
+ {
+ _mainWindowData = value;
+ OnPropertyChanged(nameof(MainWindowData));
+ }
+ }
+ }
+
+ public MainViewModel(Image imageControl)
+ {
+ ImageControl = imageControl;
+ }
+
+ // TODO: Implement proper disposal pattern
+ public void Dispose()
+ {
+ _cancellationTokenSource.Dispose();
+ GC.SuppressFinalize(this);
+ }
+
+ public void ClearSelection()
+ {
+ _cancellationTokenSource.Cancel();
+ _cancellationTokenSource = new CancellationTokenSource();
+
+ CurrentSelectedFilePath = null;
+ MainWindowData.Visibility = Visibility.Collapsed;
+ }
+
+ public bool TryUpdateSelectedFilePaths()
+ {
+ ForegroundWindowHandle = NativeMethods.GetForegroundWindow();
+
+ // TODO: Get all neighborings files in correct sorted order
+ var selectedItems = FileExplorerHelper.GetSelectedItems(ForegroundWindowHandle);
+
+ var isDifferentSelectedItems = !SelectedFilePaths.SequenceEqual(selectedItems);
+
+ if (isDifferentSelectedItems)
+ {
+ SelectedFilePaths = new LinkedList(selectedItems);
+ }
+
+ CurrentSelectedFilePath = SelectedFilePaths.First;
+
+ return isDifferentSelectedItems;
+ }
+
+ // TODO: Implement proper cancellation pattern to support quick navigation
+ public async Task RenderImageToWindowAsync(string filename)
+ {
+ IsLoading = true;
+
+ var screen = Screen.FromHandle(ForegroundWindowHandle);
+ Size maxWindowSize = new Size(screen.WpfBounds.Width * ImageScale, screen.WpfBounds.Height * ImageScale);
+
+ // TODO: Support preview or thumbnail for document files
+ if (FileTypeHelper.IsSupportedImage(Path.GetExtension(filename)))
+ {
+ await RenderSupportedImageToWindowAsync(filename, screen.Bounds, maxWindowSize);
+ }
+ else if (FileTypeHelper.IsMedia(Path.GetExtension(filename)) || FileTypeHelper.IsDocument(Path.GetExtension(filename)))
+ {
+ await RenderMediaOrDocumentToWindowAsync(filename, screen.Bounds, maxWindowSize);
+ }
+ else
+ {
+ await RenderUnsupportedFileToWindowAsync(filename, screen.Bounds, maxWindowSize);
+ }
+ }
+
+ private async Task RenderSupportedImageToWindowAsync(string filename, Rect windowBounds, Size maxWindowSize)
+ {
+ DimensionData dimensionData = await FileLoadHelper.LoadDimensionsAsync(filename);
+ if (CancellationToken.IsCancellationRequested)
+ {
+ _cancellationTokenSource = new CancellationTokenSource();
+ return;
+ }
+
+ var windowRect = dimensionData.Size.Fit(windowBounds, maxWindowSize, MinWindowSize, AllowedContentGap, MainWindowData.TitleBarHeight);
+
+ MainWindowData.Rectangle.Width = windowRect.Width;
+ MainWindowData.Rectangle.Height = windowRect.Height;
+ MainWindowData.Rectangle.Left = windowRect.Left;
+ MainWindowData.Rectangle.Top = windowRect.Top;
+
+ if (dimensionData.Size.Width > MainWindowData.Rectangle.Width || dimensionData.Size.Height > MainWindowData.Rectangle.Height)
+ {
+ ImageControl.StretchDirection = StretchDirection.Both;
+ }
+ else
+ {
+ ImageControl.StretchDirection = StretchDirection.DownOnly;
+ }
+
+ await LoadImageAsync(filename, ImageControl, dimensionData.Rotation, CancellationToken);
+ }
+
+ private async Task RenderMediaOrDocumentToWindowAsync(string filename, Rect windowBounds, Size maxWindowSize)
+ {
+ var bitmap = await FileLoadHelper.LoadThumbnailAsync(filename, true);
+ if (CancellationToken.IsCancellationRequested)
+ {
+ _cancellationTokenSource = new CancellationTokenSource();
+ return;
+ }
+
+ Bitmap = bitmap;
+
+ var imageSize = new Size(bitmap.PixelWidth, bitmap.PixelHeight);
+ var windowRect = imageSize.Fit(windowBounds, maxWindowSize, MinWindowSize, AllowedContentGap, MainWindowData.TitleBarHeight);
+
+ MainWindowData.Rectangle.Width = windowRect.Width;
+ MainWindowData.Rectangle.Height = windowRect.Height;
+ MainWindowData.Rectangle.Left = windowRect.Left;
+ MainWindowData.Rectangle.Top = windowRect.Top;
+
+ MainWindowData.Visibility = Visibility.Visible;
+ IsLoading = false;
+ }
+
+ private async Task RenderUnsupportedFileToWindowAsync(string filename, Rect windowBounds, Size maxWindowSize)
+ {
+ var contentSize = new Size(0, 0);
+ var windowRect = contentSize.Fit(windowBounds, maxWindowSize, MinWindowSize, AllowedContentGap, MainWindowData.TitleBarHeight);
+
+ MainWindowData.Rectangle.Width = windowRect.Width;
+ MainWindowData.Rectangle.Height = windowRect.Height;
+ MainWindowData.Rectangle.Left = windowRect.Left;
+ MainWindowData.Rectangle.Top = windowRect.Top;
+
+ var bitmap = await FileLoadHelper.LoadIconAsync(filename);
+ if (CancellationToken.IsCancellationRequested)
+ {
+ _cancellationTokenSource = new CancellationTokenSource();
+ return;
+ }
+
+ Bitmap = bitmap;
+ MainWindowData.Visibility = Visibility.Visible;
+ IsLoading = false;
+ }
+
+ private Task LoadImageAsync(string filename, System.Windows.Controls.Image imageControl, Rotation rotation, CancellationToken cancellationToken)
+ {
+ bool isFullImageLoaded = false;
+ bool isThumbnailLoaded = false;
+ var thumbnailLoadTask = imageControl.Dispatcher.Invoke(async () =>
+ {
+ var bitmap = await FileLoadHelper.LoadThumbnailAsync(filename, false);
+ isThumbnailLoaded = true;
+
+ if (CancellationToken.IsCancellationRequested)
+ {
+ _cancellationTokenSource = new CancellationTokenSource();
+ return;
+ }
+
+ if (!isFullImageLoaded)
+ {
+ Bitmap = bitmap;
+ MainWindowData.Visibility = Visibility.Visible;
+ IsLoading = false;
+ }
+ });
+
+ var fullImageLoadTask = imageControl.Dispatcher.Invoke(async () =>
+ {
+ var bitmap = await FileLoadHelper.LoadFullImageAsync(filename, rotation);
+ isFullImageLoaded = true;
+
+ if (CancellationToken.IsCancellationRequested)
+ {
+ _cancellationTokenSource = new CancellationTokenSource();
+ return;
+ }
+
+ Bitmap = bitmap;
+ if (!isThumbnailLoaded)
+ {
+ MainWindowData.Visibility = Visibility.Visible;
+ IsLoading = false;
+ }
+ });
+
+ return Task.WhenAll(thumbnailLoadTask, fullImageLoadTask);
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.UI.WPF/Views/MainWindow.xaml b/src/modules/peek/Peek.UI.WPF/Views/MainWindow.xaml
new file mode 100644
index 0000000000..6da3d32725
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/Views/MainWindow.xaml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/peek/Peek.UI.WPF/Views/MainWindow.xaml.cs b/src/modules/peek/Peek.UI.WPF/Views/MainWindow.xaml.cs
new file mode 100644
index 0000000000..dcfbc5cd53
--- /dev/null
+++ b/src/modules/peek/Peek.UI.WPF/Views/MainWindow.xaml.cs
@@ -0,0 +1,115 @@
+// 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.Windows;
+using System.Windows.Input;
+using interop;
+using ModernWpf.Controls;
+using Peek.UI.Extensions;
+using Peek.UI.Native;
+using Peek.UI.ViewModels;
+
+namespace Peek.UI.Views
+{
+ ///
+ /// Interaction logic for MainWindow.xaml
+ ///
+ public partial class MainWindow : Window, IDisposable
+ {
+ private readonly MainViewModel _viewModel;
+
+ public MainWindow()
+ {
+ InitializeComponent();
+
+ this.RoundCorners();
+
+ _viewModel = new MainViewModel(ImageControl);
+ _viewModel.PropertyChanged += MainViewModel_PropertyChanged;
+
+ DataContext = _viewModel;
+
+ NativeEventWaiter.WaitForEventLoop(Constants.ShowPeekEvent(), OnPeekHotkey);
+
+ Loaded += MainWindow_Loaded;
+ Closing += MainWindow_Closing;
+ KeyDown += MainWindow_KeyDown;
+ }
+
+ private void MainWindow_Loaded(object sender, RoutedEventArgs e)
+ {
+ _viewModel.MainWindowData.Visibility = Visibility.Collapsed;
+ _viewModel.MainWindowData.TitleBarHeight = TitleBar.GetHeight(this);
+ _viewModel.ImageControl = ImageControl;
+ }
+
+ private void MainWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e)
+ {
+ _viewModel.MainWindowData.Visibility = Visibility.Collapsed;
+ e.Cancel = true;
+ }
+
+ private void MainWindow_KeyDown(object? sender, KeyEventArgs e)
+ {
+ if (!e.IsRepeat && _viewModel.CurrentSelectedFilePath != null)
+ {
+ switch (e.Key)
+ {
+ case Key.Left:
+ _viewModel.CurrentSelectedFilePath = _viewModel.CurrentSelectedFilePath.GetPreviousOrLast();
+ e.Handled = true;
+ break;
+
+ case Key.Right:
+ _viewModel.CurrentSelectedFilePath = _viewModel.CurrentSelectedFilePath.GetNextOrFirst();
+ e.Handled = true;
+ break;
+
+ default: break;
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ _viewModel.Dispose();
+ GC.SuppressFinalize(this);
+ }
+
+ protected override void OnSourceInitialized(EventArgs e)
+ {
+ base.OnSourceInitialized(e);
+ this.SetToolStyle();
+ }
+
+ private async void MainViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
+ {
+ switch (e.PropertyName)
+ {
+ case nameof(MainViewModel.CurrentSelectedFilePath):
+ if (_viewModel.CurrentSelectedFilePath != null)
+ {
+ await _viewModel.RenderImageToWindowAsync(_viewModel.CurrentSelectedFilePath.Value);
+ }
+
+ break;
+ }
+ }
+
+ private void OnPeekHotkey()
+ {
+ if (IsActive && _viewModel.MainWindowData.Visibility == Visibility.Visible)
+ {
+ _viewModel.ClearSelection();
+ }
+ else
+ {
+ _viewModel.TryUpdateSelectedFilePaths();
+ }
+
+ this.BringToForeground();
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.UI/App.xaml b/src/modules/peek/Peek.UI/App.xaml
new file mode 100644
index 0000000000..7c98d9b1c1
--- /dev/null
+++ b/src/modules/peek/Peek.UI/App.xaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/peek/Peek.UI/App.xaml.cs b/src/modules/peek/Peek.UI/App.xaml.cs
new file mode 100644
index 0000000000..431df1bb7d
--- /dev/null
+++ b/src/modules/peek/Peek.UI/App.xaml.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Peek.UI
+{
+ using System;
+ using System.Diagnostics;
+ using System.Threading;
+ using ManagedCommon;
+ using Microsoft.UI.Dispatching;
+ using Microsoft.UI.Xaml;
+ using WinUIEx;
+
+ ///
+ /// Provides application-specific behavior to supplement the default Application class.
+ ///
+ public partial class App : Application
+ {
+ public static int PowerToysPID { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ /// Initializes the singleton application object. This is the first line of authored code
+ /// executed, and as such is the logical equivalent of main() or WinMain().
+ ///
+ public App()
+ {
+ this.InitializeComponent();
+ }
+
+ ///
+ /// Invoked when the application is launched.
+ ///
+ /// Details about the launch request and process.
+ protected override void OnLaunched(LaunchActivatedEventArgs args)
+ {
+ var cmdArgs = Environment.GetCommandLineArgs();
+ if (cmdArgs?.Length > 1)
+ {
+ if (int.TryParse(cmdArgs[cmdArgs.Length - 1], out int powerToysRunnerPid))
+ {
+ RunnerHelper.WaitForPowerToysRunner(powerToysRunnerPid, () =>
+ {
+ Environment.Exit(0);
+ });
+ }
+ }
+
+ window = new MainWindow();
+
+ window.Activate();
+ window.Hide();
+ }
+
+ private Window? window;
+ }
+}
diff --git a/src/modules/peek/Peek.UI/Assets/LockScreenLogo.scale-200.png b/src/modules/peek/Peek.UI/Assets/LockScreenLogo.scale-200.png
new file mode 100644
index 0000000000..7440f0d4bf
Binary files /dev/null and b/src/modules/peek/Peek.UI/Assets/LockScreenLogo.scale-200.png differ
diff --git a/src/modules/peek/Peek.UI/Assets/Peek.ico b/src/modules/peek/Peek.UI/Assets/Peek.ico
new file mode 100644
index 0000000000..daf81fdeb3
Binary files /dev/null and b/src/modules/peek/Peek.UI/Assets/Peek.ico differ
diff --git a/src/modules/peek/Peek.UI/Assets/SplashScreen.scale-200.png b/src/modules/peek/Peek.UI/Assets/SplashScreen.scale-200.png
new file mode 100644
index 0000000000..32f486a867
Binary files /dev/null and b/src/modules/peek/Peek.UI/Assets/SplashScreen.scale-200.png differ
diff --git a/src/modules/peek/Peek.UI/Assets/Square150x150Logo.scale-200.png b/src/modules/peek/Peek.UI/Assets/Square150x150Logo.scale-200.png
new file mode 100644
index 0000000000..53ee3777ea
Binary files /dev/null and b/src/modules/peek/Peek.UI/Assets/Square150x150Logo.scale-200.png differ
diff --git a/src/modules/peek/Peek.UI/Assets/Square44x44Logo.scale-200.png b/src/modules/peek/Peek.UI/Assets/Square44x44Logo.scale-200.png
new file mode 100644
index 0000000000..f713bba67f
Binary files /dev/null and b/src/modules/peek/Peek.UI/Assets/Square44x44Logo.scale-200.png differ
diff --git a/src/modules/peek/Peek.UI/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/src/modules/peek/Peek.UI/Assets/Square44x44Logo.targetsize-24_altform-unplated.png
new file mode 100644
index 0000000000..dc9f5bea0c
Binary files /dev/null and b/src/modules/peek/Peek.UI/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ
diff --git a/src/modules/peek/Peek.UI/Assets/StoreLogo.png b/src/modules/peek/Peek.UI/Assets/StoreLogo.png
new file mode 100644
index 0000000000..a4586f26bd
Binary files /dev/null and b/src/modules/peek/Peek.UI/Assets/StoreLogo.png differ
diff --git a/src/modules/peek/Peek.UI/Assets/Wide310x150Logo.scale-200.png b/src/modules/peek/Peek.UI/Assets/Wide310x150Logo.scale-200.png
new file mode 100644
index 0000000000..8b4a5d0dd5
Binary files /dev/null and b/src/modules/peek/Peek.UI/Assets/Wide310x150Logo.scale-200.png differ
diff --git a/src/modules/peek/Peek.UI/Extensions/SizeExtensions.cs b/src/modules/peek/Peek.UI/Extensions/SizeExtensions.cs
new file mode 100644
index 0000000000..fda66f05b9
--- /dev/null
+++ b/src/modules/peek/Peek.UI/Extensions/SizeExtensions.cs
@@ -0,0 +1,49 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Peek.UI.Extensions
+{
+ using Windows.Foundation;
+
+ public static class SizeExtensions
+ {
+ public static Size Fit(this Size sizeToFit, Size maxSize, Size minSize)
+ {
+ double fittedWidth = sizeToFit.Width;
+ double fittedHeight = sizeToFit.Height;
+
+ double ratioWidth = sizeToFit.Width / maxSize.Width;
+ double ratioHeight = sizeToFit.Height / maxSize.Height;
+
+ if (ratioWidth > ratioHeight)
+ {
+ if (ratioWidth > 1)
+ {
+ fittedWidth = maxSize.Width;
+ fittedHeight = sizeToFit.Height / ratioWidth;
+ }
+ }
+ else
+ {
+ if (ratioHeight > 1)
+ {
+ fittedWidth = sizeToFit.Width / ratioHeight;
+ fittedHeight = maxSize.Height;
+ }
+ }
+
+ if (fittedWidth < minSize.Width)
+ {
+ fittedWidth = minSize.Width;
+ }
+
+ if (fittedHeight < minSize.Height)
+ {
+ fittedHeight = minSize.Height;
+ }
+
+ return new Size(fittedWidth, fittedHeight);
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.UI/Extensions/WindowExtensions.cs b/src/modules/peek/Peek.UI/Extensions/WindowExtensions.cs
new file mode 100644
index 0000000000..858e449676
--- /dev/null
+++ b/src/modules/peek/Peek.UI/Extensions/WindowExtensions.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Peek.UI.Extensions
+{
+ using System.Drawing;
+ using Microsoft.UI.Xaml;
+ using Windows.Win32;
+ using Windows.Win32.Foundation;
+ using Windows.Win32.Graphics.Gdi;
+ using WinUIEx;
+
+ public static class WindowExtensions
+ {
+ public static Size GetMonitorSize(this Window window)
+ {
+ var hwnd = new HWND(window.GetWindowHandle());
+ var hwndDesktop = PInvoke.MonitorFromWindow(hwnd, MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);
+ MONITORINFO info = new ();
+ info.cbSize = 40;
+ PInvoke.GetMonitorInfo(hwndDesktop, ref info);
+ var monitorWidth = info.rcMonitor.left + info.rcMonitor.right;
+ var monitorHeight = info.rcMonitor.bottom + info.rcMonitor.top;
+
+ return new Size(monitorWidth, monitorHeight);
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.UI/Helpers/FileExplorerHelper.cs b/src/modules/peek/Peek.UI/Helpers/FileExplorerHelper.cs
new file mode 100644
index 0000000000..79975307fc
--- /dev/null
+++ b/src/modules/peek/Peek.UI/Helpers/FileExplorerHelper.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using Peek.Common.Models;
+using Peek.UI.Native;
+
+namespace Peek.UI.Helpers
+{
+ public static class FileExplorerHelper
+ {
+ public static List GetSelectedFileExplorerFiles()
+ {
+ var foregroundWindowHandle = NativeMethods.GetForegroundWindow();
+
+ var selectedItems = new List();
+ var shell = new Shell32.Shell();
+ foreach (SHDocVw.InternetExplorer window in shell.Windows())
+ {
+ if (window.HWND == (int)foregroundWindowHandle)
+ {
+ Shell32.FolderItems items = ((Shell32.IShellFolderViewDual2)window.Document).SelectedItems();
+ if (items != null && items.Count > 0)
+ {
+ foreach (Shell32.FolderItem item in items)
+ {
+ selectedItems.Add(new File(item.Path));
+ }
+ }
+ }
+ }
+
+ return selectedItems;
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.UI/MainWindow.xaml b/src/modules/peek/Peek.UI/MainWindow.xaml
new file mode 100644
index 0000000000..c261512c79
--- /dev/null
+++ b/src/modules/peek/Peek.UI/MainWindow.xaml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/peek/Peek.UI/MainWindow.xaml.cs b/src/modules/peek/Peek.UI/MainWindow.xaml.cs
new file mode 100644
index 0000000000..0ffc4af95c
--- /dev/null
+++ b/src/modules/peek/Peek.UI/MainWindow.xaml.cs
@@ -0,0 +1,98 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Peek.UI
+{
+ using System.Collections.Generic;
+ using System.Linq;
+ using interop;
+ using Microsoft.UI.Windowing;
+ using Peek.Common.Models;
+ using Peek.FilePreviewer.Models;
+ using Peek.UI.Extensions;
+ using Peek.UI.Helpers;
+ using Peek.UI.Native;
+ using Windows.Foundation;
+ using WinUIEx;
+
+ ///
+ /// An empty window that can be used on its own or navigated to within a Frame.
+ ///
+ public sealed partial class MainWindow : WindowEx
+ {
+ public MainWindow()
+ {
+ InitializeComponent();
+
+ ViewModel = new MainWindowViewModel();
+
+ NativeEventWaiter.WaitForEventLoop(Constants.ShowPeekEvent(), OnPeekHotkey);
+
+ TitleBarControl.SetToWindow(this);
+
+ AppWindow.Closing += AppWindow_Closing;
+ }
+
+ public MainWindowViewModel ViewModel { get; }
+
+ ///
+ /// Handle Peek hotkey, by toggling the window visibility and querying files when necessary.
+ ///
+ private void OnPeekHotkey()
+ {
+ if (AppWindow.IsVisible)
+ {
+ this.Hide();
+ ViewModel.Files = new List();
+ ViewModel.CurrentFile = null;
+ }
+ else
+ {
+ var fileExplorerSelectedFiles = FileExplorerHelper.GetSelectedFileExplorerFiles();
+ if (fileExplorerSelectedFiles.Count == 0)
+ {
+ return;
+ }
+
+ ViewModel.Files = fileExplorerSelectedFiles;
+ ViewModel.CurrentFile = fileExplorerSelectedFiles.First();
+ }
+ }
+
+ ///
+ /// Handle FilePreviewerSizeChanged event to adjust window size and position accordingly.
+ ///
+ /// object
+ /// PreviewSizeChangedArgs
+ private void FilePreviewer_PreviewSizeChanged(object sender, PreviewSizeChangedArgs e)
+ {
+ var requestedSize = e.WindowSizeRequested;
+ var monitorSize = this.GetMonitorSize();
+
+ // TODO: Use design-defined rules for adjusted window size
+ var titleBarHeight = TitleBarControl.ActualHeight;
+ var maxContentSize = new Size(monitorSize.Width * 0.8, (monitorSize.Height - titleBarHeight) * 0.8);
+ var minContentSize = new Size(500, 500 - titleBarHeight);
+
+ var adjustedContentSize = requestedSize.Fit(maxContentSize, minContentSize);
+
+ // TODO: Only re-center if window has not been resized by user (or use design-defined logic).
+ // TODO: Investigate why portrait images do not perfectly fit edge-to-edge
+ this.CenterOnScreen(adjustedContentSize.Width, adjustedContentSize.Height + titleBarHeight);
+ this.Show();
+ this.BringToFront();
+ }
+
+ ///
+ /// Handle AppWindow closing to prevent app termination on close.
+ ///
+ /// AppWindow
+ /// AppWindowClosingEventArgs
+ private void AppWindow_Closing(AppWindow sender, AppWindowClosingEventArgs args)
+ {
+ args.Cancel = true;
+ this.Hide();
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.UI/MainWindowViewModel.cs b/src/modules/peek/Peek.UI/MainWindowViewModel.cs
new file mode 100644
index 0000000000..0a923eda31
--- /dev/null
+++ b/src/modules/peek/Peek.UI/MainWindowViewModel.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Peek.UI
+{
+ using System.Collections.Generic;
+ using CommunityToolkit.Mvvm.ComponentModel;
+ using Peek.Common.Models;
+
+ public partial class MainWindowViewModel : ObservableObject
+ {
+ [ObservableProperty]
+ private File? currentFile;
+
+ [ObservableProperty]
+ private List files = new ();
+ }
+}
diff --git a/src/modules/peek/Peek.UI/Native/NativeMethods.cs b/src/modules/peek/Peek.UI/Native/NativeMethods.cs
new file mode 100644
index 0000000000..5335ac05c3
--- /dev/null
+++ b/src/modules/peek/Peek.UI/Native/NativeMethods.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Peek.UI.Native
+{
+ using System;
+ using System.Runtime.InteropServices;
+
+ public static class NativeMethods
+ {
+ [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
+ internal static extern IntPtr GetForegroundWindow();
+ }
+}
diff --git a/src/modules/peek/Peek.UI/NativeEventWaiter.cs b/src/modules/peek/Peek.UI/NativeEventWaiter.cs
new file mode 100644
index 0000000000..7726bede0c
--- /dev/null
+++ b/src/modules/peek/Peek.UI/NativeEventWaiter.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading;
+using Microsoft.UI.Dispatching;
+
+namespace Peek.UI.Native
+{
+ public static class NativeEventWaiter
+ {
+ public static void WaitForEventLoop(string eventName, Action callback)
+ {
+ var dispatcherQueue = DispatcherQueue.GetForCurrentThread();
+ new Thread(() =>
+ {
+ var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, eventName);
+ while (true)
+ {
+ if (eventHandle.WaitOne())
+ {
+ dispatcherQueue.TryEnqueue(() => callback());
+ }
+ }
+ }).Start();
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.UI/NativeMethods.txt b/src/modules/peek/Peek.UI/NativeMethods.txt
new file mode 100644
index 0000000000..bdccc2f7ba
--- /dev/null
+++ b/src/modules/peek/Peek.UI/NativeMethods.txt
@@ -0,0 +1,2 @@
+MonitorFromWindow
+GetMonitorInfo
\ No newline at end of file
diff --git a/src/modules/peek/Peek.UI/Package.appxmanifest b/src/modules/peek/Peek.UI/Package.appxmanifest
new file mode 100644
index 0000000000..9116adec94
--- /dev/null
+++ b/src/modules/peek/Peek.UI/Package.appxmanifest
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+ Peek.UI
+ sachaple
+ Assets\StoreLogo.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/peek/Peek.UI/Peek.UI.csproj b/src/modules/peek/Peek.UI/Peek.UI.csproj
new file mode 100644
index 0000000000..6803b6e51e
--- /dev/null
+++ b/src/modules/peek/Peek.UI/Peek.UI.csproj
@@ -0,0 +1,97 @@
+
+
+ PowerToys.Peek.UI
+ PowerToys.Peek.UI
+ PowerToys Peek UI
+ Peek.UI
+ WinExe
+ net7.0-windows10.0.19041.0
+ 10.0.19041.0
+ ..\..\..\..\$(Platform)\$(Configuration)\modules\Peek\
+ app.manifest
+ x86;x64;ARM64
+ win10-x86;win10-x64;win10-arm64
+ true
+ true
+ true
+ false
+ false
+ true
+ None
+ true
+ 10.0.19041.0
+ Enable
+
+
+
+
+
+
+
+
+
+
+ 0
+ 1
+ 50a7e9b0-70ef-11d1-b75a-00a0c90564fe
+ 0
+ tlbimp
+ false
+ true
+
+
+ 1
+ 1
+ eab22ac0-30c1-11cf-a7eb-0000c05bae0b
+ 0
+ tlbimp
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ MSBuild:Compile
+
+
+
diff --git a/src/modules/peek/Peek.UI/Strings/en-us/Resources.resw b/src/modules/peek/Peek.UI/Strings/en-us/Resources.resw
new file mode 100644
index 0000000000..5fb536c8bb
--- /dev/null
+++ b/src/modules/peek/Peek.UI/Strings/en-us/Resources.resw
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Peek
+
+
+ Peek
+
+
\ No newline at end of file
diff --git a/src/modules/peek/Peek.UI/Views/TitleBar.xaml b/src/modules/peek/Peek.UI/Views/TitleBar.xaml
new file mode 100644
index 0000000000..eb1da3b5e8
--- /dev/null
+++ b/src/modules/peek/Peek.UI/Views/TitleBar.xaml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/peek/Peek.UI/Views/TitleBar.xaml.cs b/src/modules/peek/Peek.UI/Views/TitleBar.xaml.cs
new file mode 100644
index 0000000000..fac1f78bf4
--- /dev/null
+++ b/src/modules/peek/Peek.UI/Views/TitleBar.xaml.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Peek.UI.Views
+{
+ using System;
+ using ManagedCommon;
+ using Microsoft.UI;
+ using Microsoft.UI.Windowing;
+ using Microsoft.UI.Xaml.Controls;
+ using WinUIEx;
+ using MUX = Microsoft.UI.Xaml;
+
+ public sealed partial class TitleBar : UserControl
+ {
+ public TitleBar()
+ {
+ InitializeComponent();
+ }
+
+ public void SetToWindow(MainWindow mainWindow)
+ {
+ if (AppWindowTitleBar.IsCustomizationSupported())
+ {
+ AppWindow window = mainWindow.GetAppWindow();
+ window.TitleBar.ExtendsContentIntoTitleBar = true;
+ window.TitleBar.ButtonBackgroundColor = Colors.Transparent;
+ mainWindow.SetTitleBar(this);
+ }
+ else
+ {
+ var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
+ ThemeHelpers.SetImmersiveDarkMode(hWnd, ThemeHelpers.GetAppTheme() == AppTheme.Dark);
+ Visibility = MUX.Visibility.Collapsed;
+
+ // Set window icon
+ WindowId windowId = Win32Interop.GetWindowIdFromWindow(hWnd);
+ AppWindow appWindow = AppWindow.GetFromWindowId(windowId);
+ appWindow.SetIcon("Assets/Peek.ico");
+ }
+ }
+ }
+}
diff --git a/src/modules/peek/Peek.UI/app.manifest b/src/modules/peek/Peek.UI/app.manifest
new file mode 100644
index 0000000000..c1deabb494
--- /dev/null
+++ b/src/modules/peek/Peek.UI/app.manifest
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true/PM
+ PerMonitorV2, PerMonitor
+
+
+
\ No newline at end of file
diff --git a/src/modules/peek/WIC/Classes/CLSID.cs b/src/modules/peek/WIC/Classes/CLSID.cs
new file mode 100644
index 0000000000..63bc6f575d
--- /dev/null
+++ b/src/modules/peek/WIC/Classes/CLSID.cs
@@ -0,0 +1,7 @@
+namespace WIC
+{
+ internal static class CLSID
+ {
+ public const string WICImagingFactory = "cacaf262-9370-4615-a13b-9f5539da4c0a";
+ }
+}
diff --git a/src/modules/peek/WIC/Classes/WICImagingFactory.cs b/src/modules/peek/WIC/Classes/WICImagingFactory.cs
new file mode 100644
index 0000000000..7792311c50
--- /dev/null
+++ b/src/modules/peek/WIC/Classes/WICImagingFactory.cs
@@ -0,0 +1,14 @@
+using System.Runtime.InteropServices;
+
+namespace WIC
+{
+ [ComImport]
+ [Guid(IID.IWICImagingFactory)]
+ [CoClass(typeof(WICImagingFactoryClass))]
+ public interface WICImagingFactory : IWICImagingFactory { }
+
+ [ComImport]
+ [Guid(CLSID.WICImagingFactory)]
+ [ComDefaultInterface(typeof(IWICImagingFactory))]
+ public class WICImagingFactoryClass { }
+}
diff --git a/src/modules/peek/WIC/CoTaskMemPtr.cs b/src/modules/peek/WIC/CoTaskMemPtr.cs
new file mode 100644
index 0000000000..c9029f3331
--- /dev/null
+++ b/src/modules/peek/WIC/CoTaskMemPtr.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace WIC
+{
+ internal struct CoTaskMemPtr : IDisposable
+ {
+ public static CoTaskMemPtr From(T? nullableStructure) where T : struct
+ {
+ IntPtr value;
+ if (nullableStructure.HasValue)
+ {
+ value = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(T)));
+ Marshal.StructureToPtr(nullableStructure, value, false);
+ }
+ else
+ {
+ value = IntPtr.Zero;
+ }
+ return new CoTaskMemPtr(value);
+ }
+
+ public CoTaskMemPtr(IntPtr value)
+ {
+ this.value = value;
+ }
+
+ private IntPtr value;
+
+ public static implicit operator IntPtr(CoTaskMemPtr safeIntPtr)
+ {
+ return safeIntPtr.value;
+ }
+
+ public void Dispose()
+ {
+ if (value != IntPtr.Zero)
+ {
+ Marshal.FreeCoTaskMem(value);
+ }
+ }
+ }
+}
diff --git a/src/modules/peek/WIC/Constants/ContainerFormat.cs b/src/modules/peek/WIC/Constants/ContainerFormat.cs
new file mode 100644
index 0000000000..e4b5b8da8b
--- /dev/null
+++ b/src/modules/peek/WIC/Constants/ContainerFormat.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace WIC
+{
+ public static class ContainerFormat
+ {
+ public static readonly Guid Bmp = new Guid(0x0af1d87e, 0xfcfe, 0x4188, 0xbd, 0xeb, 0xa7, 0x90, 0x64, 0x71, 0xcb, 0xe3);
+ public static readonly Guid Png = new Guid(0x1b7cfaf4, 0x713f, 0x473c, 0xbb, 0xcd, 0x61, 0x37, 0x42, 0x5f, 0xae, 0xaf);
+ public static readonly Guid Ico = new Guid(0xa3a860c4, 0x338f, 0x4c17, 0x91, 0x9a, 0xfb, 0xa4, 0xb5, 0x62, 0x8f, 0x21);
+ public static readonly Guid Jpeg = new Guid(0x19e4a5aa, 0x5662, 0x4fc5, 0xa0, 0xc0, 0x17, 0x58, 0x02, 0x8e, 0x10, 0x57);
+ public static readonly Guid Tiff = new Guid(0x163bcc30, 0xe2e9, 0x4f0b, 0x96, 0x1d, 0xa3, 0xe9, 0xfd, 0xb7, 0x88, 0xa3);
+ public static readonly Guid Gif = new Guid(0x1f8a5601, 0x7d4d, 0x4cbd, 0x9c, 0x82, 0x1b, 0xc8, 0xd4, 0xee, 0xb9, 0xa5);
+ public static readonly Guid Wmp = new Guid(0x57a37caa, 0x367a, 0x4540, 0x91, 0x6b, 0xf1, 0x83, 0xc5, 0x09, 0x3a, 0x4b);
+ public static readonly Guid Dds = new Guid(0x9967cb95, 0x2e85, 0x4ac8, 0x8c, 0xa2, 0x83, 0xd7, 0xcc, 0xd4, 0x25, 0xc9);
+ }
+}
diff --git a/src/modules/peek/WIC/Constants/HResult.cs b/src/modules/peek/WIC/Constants/HResult.cs
new file mode 100644
index 0000000000..341c8530d2
--- /dev/null
+++ b/src/modules/peek/WIC/Constants/HResult.cs
@@ -0,0 +1,7 @@
+namespace WIC
+{
+ public struct HResult
+ {
+ public const int WINCODEC_ERR_PROPERTYNOTFOUND = unchecked((int)0x88982F40);
+ }
+}
diff --git a/src/modules/peek/WIC/Constants/Vendor.cs b/src/modules/peek/WIC/Constants/Vendor.cs
new file mode 100644
index 0000000000..6377f0b735
--- /dev/null
+++ b/src/modules/peek/WIC/Constants/Vendor.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace WIC
+{
+ public static class Vendor
+ {
+ public static readonly Guid Microsoft = new Guid(0xf0e749ca, 0xedef, 0x4589, 0xa7, 0x3a, 0xee, 0xe, 0x62, 0x6a, 0x2a, 0x2b);
+ public static readonly Guid MicrosoftBuiltIn = new Guid(0x257a30fd, 0x6b6, 0x462b, 0xae, 0xa4, 0x63, 0xf7, 0xb, 0x86, 0xe5, 0x33);
+ }
+}
diff --git a/src/modules/peek/WIC/Constants/WICPixelFormat.cs b/src/modules/peek/WIC/Constants/WICPixelFormat.cs
new file mode 100644
index 0000000000..1e6701ec44
--- /dev/null
+++ b/src/modules/peek/WIC/Constants/WICPixelFormat.cs
@@ -0,0 +1,98 @@
+using System;
+
+namespace WIC.Constants
+{
+ public static class WICPixelFormat
+ {
+ public static readonly Guid WICPixelFormatDontCare = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x00);
+ public static readonly Guid WICPixelFormat1bppIndexed = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x01);
+ public static readonly Guid WICPixelFormat2bppIndexed = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x02);
+ public static readonly Guid WICPixelFormat4bppIndexed = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x03);
+ public static readonly Guid WICPixelFormat8bppIndexed = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x04);
+ public static readonly Guid WICPixelFormatBlackWhite = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x05);
+ public static readonly Guid WICPixelFormat2bppGray = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x06);
+ public static readonly Guid WICPixelFormat4bppGray = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x07);
+ public static readonly Guid WICPixelFormat8bppGray = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x08);
+ public static readonly Guid WICPixelFormat8bppAlpha = new Guid(0xe6cd0116, 0xeeba, 0x4161, 0xaa, 0x85, 0x27, 0xdd, 0x9f, 0xb3, 0xa8, 0x95);
+ public static readonly Guid WICPixelFormat16bppBGR555 = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x09);
+ public static readonly Guid WICPixelFormat16bppBGR565 = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x0a);
+ public static readonly Guid WICPixelFormat16bppBGRA5551 = new Guid(0x05ec7c2b, 0xf1e6, 0x4961, 0xad, 0x46, 0xe1, 0xcc, 0x81, 0x0a, 0x87, 0xd2);
+ public static readonly Guid WICPixelFormat16bppGray = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x0b);
+ public static readonly Guid WICPixelFormat24bppBGR = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x0c);
+ public static readonly Guid WICPixelFormat24bppRGB = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x0d);
+ public static readonly Guid WICPixelFormat32bppBGR = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x0e);
+ public static readonly Guid WICPixelFormat32bppBGRA = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x0f);
+ public static readonly Guid WICPixelFormat32bppPBGRA = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x10);
+ public static readonly Guid WICPixelFormat32bppGrayFloat = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x11);
+ public static readonly Guid WICPixelFormat32bppRGB = new Guid(0xd98c6b95, 0x3efe, 0x47d6, 0xbb, 0x25, 0xeb, 0x17, 0x48, 0xab, 0x0c, 0xf1);
+ public static readonly Guid WICPixelFormat32bppRGBA = new Guid(0xf5c7ad2d, 0x6a8d, 0x43dd, 0xa7, 0xa8, 0xa2, 0x99, 0x35, 0x26, 0x1a, 0xe9);
+ public static readonly Guid WICPixelFormat32bppPRGBA = new Guid(0x3cc4a650, 0xa527, 0x4d37, 0xa9, 0x16, 0x31, 0x42, 0xc7, 0xeb, 0xed, 0xba);
+ public static readonly Guid WICPixelFormat48bppRGB = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x15);
+ public static readonly Guid WICPixelFormat48bppBGR = new Guid(0xe605a384, 0xb468, 0x46ce, 0xbb, 0x2e, 0x36, 0xf1, 0x80, 0xe6, 0x43, 0x13);
+ public static readonly Guid WICPixelFormat64bppRGB = new Guid(0xa1182111, 0x186d, 0x4d42, 0xbc, 0x6a, 0x9c, 0x83, 0x03, 0xa8, 0xdf, 0xf9);
+ public static readonly Guid WICPixelFormat64bppRGBA = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x16);
+ public static readonly Guid WICPixelFormat64bppBGRA = new Guid(0x1562ff7c, 0xd352, 0x46f9, 0x97, 0x9e, 0x42, 0x97, 0x6b, 0x79, 0x22, 0x46);
+ public static readonly Guid WICPixelFormat64bppPRGBA = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x17);
+ public static readonly Guid WICPixelFormat64bppPBGRA = new Guid(0x8c518e8e, 0xa4ec, 0x468b, 0xae, 0x70, 0xc9, 0xa3, 0x5a, 0x9c, 0x55, 0x30);
+ public static readonly Guid WICPixelFormat16bppGrayFixedPoint = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x13);
+ public static readonly Guid WICPixelFormat32bppBGR101010 = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x14);
+ public static readonly Guid WICPixelFormat48bppRGBFixedPoint = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x12);
+ public static readonly Guid WICPixelFormat48bppBGRFixedPoint = new Guid(0x49ca140e, 0xcab6, 0x493b, 0x9d, 0xdf, 0x60, 0x18, 0x7c, 0x37, 0x53, 0x2a);
+ public static readonly Guid WICPixelFormat96bppRGBFixedPoint = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x18);
+ public static readonly Guid WICPixelFormat96bppRGBFloat = new Guid(0xe3fed78f, 0xe8db, 0x4acf, 0x84, 0xc1, 0xe9, 0x7f, 0x61, 0x36, 0xb3, 0x27);
+ public static readonly Guid WICPixelFormat128bppRGBAFloat = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x19);
+ public static readonly Guid WICPixelFormat128bppPRGBAFloat = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x1a);
+ public static readonly Guid WICPixelFormat128bppRGBFloat = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x1b);
+ public static readonly Guid WICPixelFormat32bppCMYK = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x1c);
+ public static readonly Guid WICPixelFormat64bppRGBAFixedPoint = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x1d);
+ public static readonly Guid WICPixelFormat64bppBGRAFixedPoint = new Guid(0x356de33c, 0x54d2, 0x4a23, 0xbb, 0x4, 0x9b, 0x7b, 0xf9, 0xb1, 0xd4, 0x2d);
+ public static readonly Guid WICPixelFormat64bppRGBFixedPoint = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x40);
+ public static readonly Guid WICPixelFormat128bppRGBAFixedPoint = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x1e);
+ public static readonly Guid WICPixelFormat128bppRGBFixedPoint = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x41);
+ public static readonly Guid WICPixelFormat64bppRGBAHalf = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x3a);
+ public static readonly Guid WICPixelFormat64bppPRGBAHalf = new Guid(0x58ad26c2, 0xc623, 0x4d9d, 0xb3, 0x20, 0x38, 0x7e, 0x49, 0xf8, 0xc4, 0x42);
+ public static readonly Guid WICPixelFormat64bppRGBHalf = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x42);
+ public static readonly Guid WICPixelFormat48bppRGBHalf = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x3b);
+ public static readonly Guid WICPixelFormat32bppRGBE = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x3d);
+ public static readonly Guid WICPixelFormat16bppGrayHalf = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x3e);
+ public static readonly Guid WICPixelFormat32bppGrayFixedPoint = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x3f);
+ public static readonly Guid WICPixelFormat32bppRGBA1010102 = new Guid(0x25238D72, 0xFCF9, 0x4522, 0xb5, 0x14, 0x55, 0x78, 0xe5, 0xad, 0x55, 0xe0);
+ public static readonly Guid WICPixelFormat32bppRGBA1010102XR = new Guid(0x00DE6B9A, 0xC101, 0x434b, 0xb5, 0x02, 0xd0, 0x16, 0x5e, 0xe1, 0x12, 0x2c);
+ public static readonly Guid WICPixelFormat32bppR10G10B10A2 = new Guid(0x604e1bb5, 0x8a3c, 0x4b65, 0xb1, 0x1c, 0xbc, 0x0b, 0x8d, 0xd7, 0x5b, 0x7f);
+ public static readonly Guid WICPixelFormat32bppR10G10B10A2HDR10 = new Guid(0x9c215c5d, 0x1acc, 0x4f0e, 0xa4, 0xbc, 0x70, 0xfb, 0x3a, 0xe8, 0xfd, 0x28);
+ public static readonly Guid WICPixelFormat64bppCMYK = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x1f);
+ public static readonly Guid WICPixelFormat24bpp3Channels = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x20);
+ public static readonly Guid WICPixelFormat32bpp4Channels = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x21);
+ public static readonly Guid WICPixelFormat40bpp5Channels = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x22);
+ public static readonly Guid WICPixelFormat48bpp6Channels = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x23);
+ public static readonly Guid WICPixelFormat56bpp7Channels = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x24);
+ public static readonly Guid WICPixelFormat64bpp8Channels = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x25);
+ public static readonly Guid WICPixelFormat48bpp3Channels = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x26);
+ public static readonly Guid WICPixelFormat64bpp4Channels = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x27);
+ public static readonly Guid WICPixelFormat80bpp5Channels = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x28);
+ public static readonly Guid WICPixelFormat96bpp6Channels = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x29);
+ public static readonly Guid WICPixelFormat112bpp7Channels = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x2a);
+ public static readonly Guid WICPixelFormat128bpp8Channels = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x2b);
+ public static readonly Guid WICPixelFormat40bppCMYKAlpha = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x2c);
+ public static readonly Guid WICPixelFormat80bppCMYKAlpha = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x2d);
+ public static readonly Guid WICPixelFormat32bpp3ChannelsAlpha = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x2e);
+ public static readonly Guid WICPixelFormat40bpp4ChannelsAlpha = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x2f);
+ public static readonly Guid WICPixelFormat48bpp5ChannelsAlpha = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x30);
+ public static readonly Guid WICPixelFormat56bpp6ChannelsAlpha = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x31);
+ public static readonly Guid WICPixelFormat64bpp7ChannelsAlpha = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x32);
+ public static readonly Guid WICPixelFormat72bpp8ChannelsAlpha = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x33);
+ public static readonly Guid WICPixelFormat64bpp3ChannelsAlpha = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x34);
+ public static readonly Guid WICPixelFormat80bpp4ChannelsAlpha = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x35);
+ public static readonly Guid WICPixelFormat96bpp5ChannelsAlpha = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x36);
+ public static readonly Guid WICPixelFormat112bpp6ChannelsAlpha = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x37);
+ public static readonly Guid WICPixelFormat128bpp7ChannelsAlpha = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x38);
+ public static readonly Guid WICPixelFormat144bpp8ChannelsAlpha = new Guid(0x6fddc324, 0x4e03, 0x4bfe, 0xb1, 0x85, 0x3d, 0x77, 0x76, 0x8d, 0xc9, 0x39);
+ public static readonly Guid WICPixelFormat8bppY = new Guid(0x91B4DB54, 0x2DF9, 0x42F0, 0xB4, 0x49, 0x29, 0x09, 0xBB, 0x3D, 0xF8, 0x8E);
+ public static readonly Guid WICPixelFormat8bppCb = new Guid(0x1339F224, 0x6BFE, 0x4C3E, 0x93, 0x02, 0xE4, 0xF3, 0xA6, 0xD0, 0xCA, 0x2A);
+ public static readonly Guid WICPixelFormat8bppCr = new Guid(0xB8145053, 0x2116, 0x49F0, 0x88, 0x35, 0xED, 0x84, 0x4B, 0x20, 0x5C, 0x51);
+ public static readonly Guid WICPixelFormat16bppCbCr = new Guid(0xFF95BA6E, 0x11E0, 0x4263, 0xBB, 0x45, 0x01, 0x72, 0x1F, 0x34, 0x60, 0xA4);
+ public static readonly Guid WICPixelFormat16bppYQuantizedDctCoefficients = new Guid(0xA355F433, 0x48E8, 0x4A42, 0x84, 0xD8, 0xE2, 0xAA, 0x26, 0xCA, 0x80, 0xA4);
+ public static readonly Guid WICPixelFormat16bppCbQuantizedDctCoefficients = new Guid(0xD2C4FF61, 0x56A5, 0x49C2, 0x8B, 0x5C, 0x4C, 0x19, 0x25, 0x96, 0x48, 0x37);
+ public static readonly Guid WICPixelFormat16bppCrQuantizedDctCoefficients = new Guid(0x2FE354F0, 0x1680, 0x42D8, 0x92, 0x31, 0xE7, 0x3C, 0x05, 0x65, 0xBF, 0xC1);
+ }
+}
diff --git a/src/modules/peek/WIC/Enumerations/ExifColorSpace.cs b/src/modules/peek/WIC/Enumerations/ExifColorSpace.cs
new file mode 100644
index 0000000000..ebf6047cbb
--- /dev/null
+++ b/src/modules/peek/WIC/Enumerations/ExifColorSpace.cs
@@ -0,0 +1,8 @@
+namespace WIC
+{
+ public enum ExifColorSpace : int
+ {
+ SRGB = 1,
+ AdobeSRGB = 2,
+ }
+}
diff --git a/src/modules/peek/WIC/Enumerations/LOCKTYPE.cs b/src/modules/peek/WIC/Enumerations/LOCKTYPE.cs
new file mode 100644
index 0000000000..9ce372bb98
--- /dev/null
+++ b/src/modules/peek/WIC/Enumerations/LOCKTYPE.cs
@@ -0,0 +1,14 @@
+using System;
+using System.ComponentModel;
+
+namespace WIC
+{
+ [Flags]
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public enum LOCKTYPE : int
+ {
+ LOCK_WRITE = 1,
+ LOCK_EXCLUSIVE = 2,
+ LOCK_ONLYONCE = 4,
+ }
+}
diff --git a/src/modules/peek/WIC/Enumerations/MetadataCreationAndPersistOptions.cs b/src/modules/peek/WIC/Enumerations/MetadataCreationAndPersistOptions.cs
new file mode 100644
index 0000000000..01a10508ac
--- /dev/null
+++ b/src/modules/peek/WIC/Enumerations/MetadataCreationAndPersistOptions.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace WIC
+{
+ [Flags]
+ public enum MetadataCreationAndPersistOptions : int
+ {
+ WICMetadataCreationDefault = 0x00000000,
+ WICMetadataCreationAllowUnknown = WICMetadataCreationDefault,
+ WICMetadataCreationFailUnknown = 0x00010000,
+
+ WICPersistOptionDefault = 0x00000000,
+ WICPersistOptionLittleEndian = 0x00000000,
+ WICPersistOptionBigEndian = 0x00000001,
+ WICPersistOptionStrictFormat = 0x00000002,
+ WICPersistOptionNoCacheStream = 0x00000004,
+ WICPersistOptionPreferUTF8 = 0x00000008,
+ }
+}
diff --git a/src/modules/peek/WIC/Enumerations/STATFLAG.cs b/src/modules/peek/WIC/Enumerations/STATFLAG.cs
new file mode 100644
index 0000000000..8f6afee364
--- /dev/null
+++ b/src/modules/peek/WIC/Enumerations/STATFLAG.cs
@@ -0,0 +1,12 @@
+using System.ComponentModel;
+
+namespace WIC
+{
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public enum STATFLAG : int
+ {
+ STATFLAG_DEFAULT = 0,
+ STATFLAG_NONAME = 1,
+ STATFLAG_NOOPEN = 2,
+ }
+}
diff --git a/src/modules/peek/WIC/Enumerations/STGC.cs b/src/modules/peek/WIC/Enumerations/STGC.cs
new file mode 100644
index 0000000000..4010f835f5
--- /dev/null
+++ b/src/modules/peek/WIC/Enumerations/STGC.cs
@@ -0,0 +1,16 @@
+using System;
+using System.ComponentModel;
+
+namespace WIC
+{
+ [Flags]
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public enum STGC : int
+ {
+ STGC_DEFAULT = 0,
+ STGC_OVERWRITE = 1,
+ STGC_ONLYIFCURRENT = 2,
+ STGC_DANGEROUSLYCOMMITMERELYTODISKCACHE = 4,
+ STGC_CONSOLIDATE = 8,
+ }
+}
diff --git a/src/modules/peek/WIC/Enumerations/STGM.cs b/src/modules/peek/WIC/Enumerations/STGM.cs
new file mode 100644
index 0000000000..03cdb33e0b
--- /dev/null
+++ b/src/modules/peek/WIC/Enumerations/STGM.cs
@@ -0,0 +1,15 @@
+using System;
+using System.ComponentModel;
+
+namespace WIC
+{
+ [Flags]
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public enum STGM : int
+ {
+ STGM_READ = 0x00000000,
+ STGM_WRITE = 0x00000001,
+ STGM_READWRITE = 0x00000002,
+ #warning `STGM`: Enumeration incomplete. Consider adding all values.
+ }
+}
diff --git a/src/modules/peek/WIC/Enumerations/STGTY.cs b/src/modules/peek/WIC/Enumerations/STGTY.cs
new file mode 100644
index 0000000000..989332553b
--- /dev/null
+++ b/src/modules/peek/WIC/Enumerations/STGTY.cs
@@ -0,0 +1,13 @@
+using System.ComponentModel;
+
+namespace WIC
+{
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public enum STGTY : int
+ {
+ STGTY_STORAGE = 1,
+ STGTY_STREAM = 2,
+ STGTY_LOCKBYTES = 3,
+ STGTY_PROPERTY = 4,
+ }
+}
diff --git a/src/modules/peek/WIC/Enumerations/STREAM_SEEK.cs b/src/modules/peek/WIC/Enumerations/STREAM_SEEK.cs
new file mode 100644
index 0000000000..e92c2aab92
--- /dev/null
+++ b/src/modules/peek/WIC/Enumerations/STREAM_SEEK.cs
@@ -0,0 +1,12 @@
+using System.ComponentModel;
+
+namespace WIC
+{
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public enum STREAM_SEEK : int
+ {
+ STREAM_SEEK_SET = 0,
+ STREAM_SEEK_CUR = 1,
+ STREAM_SEEK_END = 2,
+ }
+}
diff --git a/src/modules/peek/WIC/Enumerations/StreamAccessMode.cs b/src/modules/peek/WIC/Enumerations/StreamAccessMode.cs
new file mode 100644
index 0000000000..e79321d0df
--- /dev/null
+++ b/src/modules/peek/WIC/Enumerations/StreamAccessMode.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace WIC
+{
+ [Flags]
+ public enum StreamAccessMode : int
+ {
+ GENERIC_WRITE = 0x40000000,
+ GENERIC_READ = unchecked((int)0x80000000U),
+ }
+}
diff --git a/src/modules/peek/WIC/Enumerations/WICBitmapAlphaChannelOption.cs b/src/modules/peek/WIC/Enumerations/WICBitmapAlphaChannelOption.cs
new file mode 100644
index 0000000000..4623c5605e
--- /dev/null
+++ b/src/modules/peek/WIC/Enumerations/WICBitmapAlphaChannelOption.cs
@@ -0,0 +1,9 @@
+namespace WIC
+{
+ public enum WICBitmapAlphaChannelOption : int
+ {
+ WICBitmapUseAlpha = 0x00000000,
+ WICBitmapUsePremultipliedAlpha = 0x00000001,
+ WICBitmapIgnoreAlpha = 0x00000002,
+ }
+}
diff --git a/src/modules/peek/WIC/Enumerations/WICBitmapCreateCacheOption.cs b/src/modules/peek/WIC/Enumerations/WICBitmapCreateCacheOption.cs
new file mode 100644
index 0000000000..09bf9b71ea
--- /dev/null
+++ b/src/modules/peek/WIC/Enumerations/WICBitmapCreateCacheOption.cs
@@ -0,0 +1,9 @@
+namespace WIC
+{
+ public enum WICBitmapCreateCacheOption : int
+ {
+ WICBitmapNoCache = 0x00000000,
+ WICBitmapCacheOnDemand = 0x00000001,
+ WICBitmapCacheOnLoad = 0x00000002,
+ }
+}
diff --git a/src/modules/peek/WIC/Enumerations/WICBitmapDecoderCapabilities.cs b/src/modules/peek/WIC/Enumerations/WICBitmapDecoderCapabilities.cs
new file mode 100644
index 0000000000..19b9c30652
--- /dev/null
+++ b/src/modules/peek/WIC/Enumerations/WICBitmapDecoderCapabilities.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace WIC
+{
+ [Flags]
+ public enum WICBitmapDecoderCapabilities : int
+ {
+ WICBitmapDecoderCapabilitySameEncoder = 0x00000001,
+ WICBitmapDecoderCapabilityCanDecodeAllImages = 0x00000002,
+ WICBitmapDecoderCapabilityCanDecodeSomeImages = 0x00000004,
+ WICBitmapDecoderCapabilityCanEnumerateMetadata = 0x00000008,
+ WICBitmapDecoderCapabilityCanDecodeThumbnail = 0x00000010,
+ }
+}
diff --git a/src/modules/peek/WIC/Enumerations/WICBitmapDitherType.cs b/src/modules/peek/WIC/Enumerations/WICBitmapDitherType.cs
new file mode 100644
index 0000000000..c9441c6686
--- /dev/null
+++ b/src/modules/peek/WIC/Enumerations/WICBitmapDitherType.cs
@@ -0,0 +1,19 @@
+namespace WIC
+{
+ public enum WICBitmapDitherType : int
+ {
+ WICBitmapDitherTypeNone = 0x00000000,
+ WICBitmapDitherTypeSolid = 0x00000000,
+
+ WICBitmapDitherTypeOrdered4x4 = 0x00000001,
+
+ WICBitmapDitherTypeOrdered8x8 = 0x00000002,
+ WICBitmapDitherTypeOrdered16x16 = 0x00000003,
+ WICBitmapDitherTypeSpiral4x4 = 0x00000004,
+ WICBitmapDitherTypeSpiral8x8 = 0x00000005,
+ WICBitmapDitherTypeDualSpiral4x4 = 0x00000006,
+ WICBitmapDitherTypeDualSpiral8x8 = 0x00000007,
+
+ WICBitmapDitherTypeErrorDiffusion = 0x00000008,
+ }
+}
diff --git a/src/modules/peek/WIC/Enumerations/WICBitmapEncoderCacheOption.cs b/src/modules/peek/WIC/Enumerations/WICBitmapEncoderCacheOption.cs
new file mode 100644
index 0000000000..922235cfee
--- /dev/null
+++ b/src/modules/peek/WIC/Enumerations/WICBitmapEncoderCacheOption.cs
@@ -0,0 +1,9 @@
+namespace WIC
+{
+ public enum WICBitmapEncoderCacheOption : int
+ {
+ WICBitmapEncoderCacheInMemory = 0x00000000,
+ WICBitmapEncoderCacheTempFile = 0x00000001,
+ WICBitmapEncoderNoCache = 0x00000002,
+ }
+}
\ No newline at end of file
diff --git a/src/modules/peek/WIC/Enumerations/WICBitmapInterpolationMode.cs b/src/modules/peek/WIC/Enumerations/WICBitmapInterpolationMode.cs
new file mode 100644
index 0000000000..85a17f4f48
--- /dev/null
+++ b/src/modules/peek/WIC/Enumerations/WICBitmapInterpolationMode.cs
@@ -0,0 +1,14 @@
+namespace WIC
+{
+ public enum WICBitmapInterpolationMode : int
+ {
+ WICBitmapInterpolationModeNearestNeighbor = 0x00000000,
+ WICBitmapInterpolationModeLinear = 0x00000001,
+ WICBitmapInterpolationModeCubic = 0x00000002,
+ WICBitmapInterpolationModeFant = 0x00000003,
+ ///
+ /// Supported beginning with Windows 10.
+ ///
+ WICBitmapInterpolationModeHighQualityCubic = 0x00000004,
+ }
+}
diff --git a/src/modules/peek/WIC/Enumerations/WICBitmapLockFlags.cs b/src/modules/peek/WIC/Enumerations/WICBitmapLockFlags.cs
new file mode 100644
index 0000000000..06ff6ebde1
--- /dev/null
+++ b/src/modules/peek/WIC/Enumerations/WICBitmapLockFlags.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace WIC
+{
+ [Flags]
+ public enum WICBitmapLockFlags : int
+ {
+ WICBitmapLockRead = 0x00000001,
+ WICBitmapLockWrite = 0x00000002,
+ }
+}
diff --git a/src/modules/peek/WIC/Enumerations/WICBitmapPaletteType.cs b/src/modules/peek/WIC/Enumerations/WICBitmapPaletteType.cs
new file mode 100644
index 0000000000..4a7a81bf72
--- /dev/null
+++ b/src/modules/peek/WIC/Enumerations/WICBitmapPaletteType.cs
@@ -0,0 +1,20 @@
+namespace WIC
+{
+ public enum WICBitmapPaletteType : int
+ {
+ WICBitmapPaletteTypeCustom = 0x00000000,
+ WICBitmapPaletteTypeMedianCut = 0x00000001,
+ WICBitmapPaletteTypeFixedBW = 0x00000002,
+ WICBitmapPaletteTypeFixedHalftone8 = 0x00000003,
+ WICBitmapPaletteTypeFixedHalftone27 = 0x00000004,
+ WICBitmapPaletteTypeFixedHalftone64 = 0x00000005,
+ WICBitmapPaletteTypeFixedHalftone125 = 0x00000006,
+ WICBitmapPaletteTypeFixedHalftone216 = 0x00000007,
+ WICBitmapPaletteTypeFixedWebPalette = WICBitmapPaletteTypeFixedHalftone216,
+ WICBitmapPaletteTypeFixedHalftone252 = 0x00000008,
+ WICBitmapPaletteTypeFixedHalftone256 = 0x00000009,
+ WICBitmapPaletteTypeFixedGray4 = 0x0000000A,
+ WICBitmapPaletteTypeFixedGray16 = 0x0000000B,
+ WICBitmapPaletteTypeFixedGray256 = 0x0000000C,
+ }
+}
diff --git a/src/modules/peek/WIC/Enumerations/WICBitmapTransformOptions.cs b/src/modules/peek/WIC/Enumerations/WICBitmapTransformOptions.cs
new file mode 100644
index 0000000000..474d7480c3
--- /dev/null
+++ b/src/modules/peek/WIC/Enumerations/WICBitmapTransformOptions.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace WIC
+{
+ [Flags]
+ public enum WICBitmapTransformOptions : int
+ {
+ WICBitmapTransformRotate0 = 0x00000000,
+ WICBitmapTransformRotate90 = 0x00000001,
+ WICBitmapTransformRotate180 = 0x00000002,
+ WICBitmapTransformRotate270 = 0x00000003,
+
+ WICBitmapTransformFlipHorizontal = 0x00000008,
+
+ WICBitmapTransformFlipVertical = 0x00000010,
+ }
+}
diff --git a/src/modules/peek/WIC/Enumerations/WICColorContextType.cs b/src/modules/peek/WIC/Enumerations/WICColorContextType.cs
new file mode 100644
index 0000000000..836f88d9fa
--- /dev/null
+++ b/src/modules/peek/WIC/Enumerations/WICColorContextType.cs
@@ -0,0 +1,9 @@
+namespace WIC
+{
+ public enum WICColorContextType : int
+ {
+ WICColorContextUninitialized = 0x00000000,
+ WICColorContextProfile = 0x00000001,
+ WICColorContextExifColorSpace = 0x00000002,
+ }
+}
diff --git a/src/modules/peek/WIC/Enumerations/WICComponentEnumerateOptions.cs b/src/modules/peek/WIC/Enumerations/WICComponentEnumerateOptions.cs
new file mode 100644
index 0000000000..2159ac2b8d
--- /dev/null
+++ b/src/modules/peek/WIC/Enumerations/WICComponentEnumerateOptions.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace WIC
+{
+ [Flags]
+ public enum WICComponentEnumerateOptions : int
+ {
+ WICComponentEnumerateDefault = 0x00000000,
+ WICComponentEnumerateRefresh = 0x00000001,
+ WICComponentEnumerateBuiltInOnly = 0x20000000,
+ WICComponentEnumerateUnsigned = 0x40000000,
+ WICComponentEnumerateDisabled = unchecked((int)0x80000000U),
+ }
+}
diff --git a/src/modules/peek/WIC/Enumerations/WICComponentSigning.cs b/src/modules/peek/WIC/Enumerations/WICComponentSigning.cs
new file mode 100644
index 0000000000..68bc08516f
--- /dev/null
+++ b/src/modules/peek/WIC/Enumerations/WICComponentSigning.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace WIC
+{
+ [Flags]
+ public enum WICComponentSigning : int
+ {
+ WICComponentSigned = 0x00000001,
+ WICComponentUnsigned = 0x00000002,
+ WICComponentSafe = 0x00000004,
+ WICComponentDisabled = unchecked((int)0x80000000U),
+ }
+}
diff --git a/src/modules/peek/WIC/Enumerations/WICComponentType.cs b/src/modules/peek/WIC/Enumerations/WICComponentType.cs
new file mode 100644
index 0000000000..0448c85129
--- /dev/null
+++ b/src/modules/peek/WIC/Enumerations/WICComponentType.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace WIC
+{
+ [Flags]
+ public enum WICComponentType : int
+ {
+ WICDecoder = 0x00000001,
+ WICEncoder = 0x00000002,
+ WICPixelFormatConverter = 0x00000004,
+ WICMetadataReader = 0x00000008,
+ WICMetadataWriter = 0x00000010,
+ WICPixelFormat = 0x00000020,
+ WICAllComponents = 0x0000003F,
+ }
+}
diff --git a/src/modules/peek/WIC/Enumerations/WICDecodeOptions.cs b/src/modules/peek/WIC/Enumerations/WICDecodeOptions.cs
new file mode 100644
index 0000000000..84ba889161
--- /dev/null
+++ b/src/modules/peek/WIC/Enumerations/WICDecodeOptions.cs
@@ -0,0 +1,8 @@
+namespace WIC
+{
+ public enum WICDecodeOptions : int
+ {
+ WICDecodeMetadataCacheOnDemand = 0x00000000,
+ WICDecodeMetadataCacheOnLoad = 0x00000001,
+ }
+}
diff --git a/src/modules/peek/WIC/Enumerations/WICMetadataCreationOptions.cs b/src/modules/peek/WIC/Enumerations/WICMetadataCreationOptions.cs
new file mode 100644
index 0000000000..f6c2bc3c2b
--- /dev/null
+++ b/src/modules/peek/WIC/Enumerations/WICMetadataCreationOptions.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace WIC
+{
+ [Flags]
+ public enum WICMetadataCreationOptions : int
+ {
+ WICMetadataCreationDefault = 0x00000000,
+ WICMetadataCreationAllowUnknown = WICMetadataCreationDefault,
+ WICMetadataCreationFailUnknown = 0x00010000,
+ WICMetadataCreationMask = unchecked((int)0xFFFF0000),
+ }
+}
diff --git a/src/modules/peek/WIC/Extensions/IEnumStringExtensions.cs b/src/modules/peek/WIC/Extensions/IEnumStringExtensions.cs
new file mode 100644
index 0000000000..35c507c365
--- /dev/null
+++ b/src/modules/peek/WIC/Extensions/IEnumStringExtensions.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+
+namespace WIC
+{
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public static class IEnumStringExtensions
+ {
+ public static IEnumerable AsEnumerable(this IEnumString enumString)
+ {
+ var buffer = new string[1];
+ for (;;)
+ {
+ int length;
+ enumString.Next(1, buffer, out length);
+ if (length != 1) break;
+ yield return buffer[0];
+ }
+ }
+ }
+}
diff --git a/src/modules/peek/WIC/Extensions/IEnumUnknownExtensions.cs b/src/modules/peek/WIC/Extensions/IEnumUnknownExtensions.cs
new file mode 100644
index 0000000000..a581a343e6
--- /dev/null
+++ b/src/modules/peek/WIC/Extensions/IEnumUnknownExtensions.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+
+namespace WIC
+{
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public static class IEnumUnknownExtensions
+ {
+ public static IEnumerable