[Peek] Powertoys Peek MVP (#25922)

* Peek (#22498)

* Add peek dll project

* add spacebar preview and launch on hotkey press

* add todo

* add process handle to handle continuous press of hotkey

* add tool to stop all powertoys processes

* Add a blank Peek page and update nav menu

* Add some initial content to Peek page including a toggle

* refactor settings parsing

* rename spacebar peek to peek viewer

* rename script to stop powertoys processes

* remove tool

* Adding FileUtils for retrieving selected file in File Explorer

* Remove unnecessary SndPeekSettings

* Add shortcut setting

* Set the shortcut to ctrl+space

* Launching viewer with selected FE file

* Add PeekUI WinUI3 project with interop events

* Moving FileTypeUtils into PeekFileUtils project

* execute winui3 app on hotkey

* Fix paths with spaces

* remove winui3 project

* Resolve comment

* add wpf app with toggle visibility on hotkey

* fix visibility on startup

* remove window properties and add todos

* Fixed hidden extension and system file handling

* wip

* Add working WPF app with FileExplorer querying

* remove c++ projects

* Move native awaiter

* Working Image control with image files

* Resize and move window based on explorer monitor

* Image render, window positioning and sizing clean up

* add window management logic and selection logic

* add extension methods to add circular iterating capability to linkedlistnode

* Add OnArrowKeyPresshandler

* Added titlebar with file name and scaling with titlebar height

* fix flashing window on startup and process kept alive when powertoys exits

* remove wait for debugger loop in ui

* Add KeyIsDown method

* Fix KeyDown issue with Key handled and check for repeat

* Add thumbnail logic

* Add all folder items if only one item is selected

* File type helper

* Using hresult

* Add cancellation and rotation handling

* Use extension instead of path

* fIX CONFLICTS

* Fixing some file type checks

* Add new icon for Peek

* Update page with the new Peek icon

* Initialize IsEnabled and hook ActivationShortcut to dllmain

* add icon to taskbar and titlebar

* Add theme sensitive backgrounds

* rename event handlers

* add settings image

* Move window data into obserable object

* Refactor viewmodel, interop and helpers

* Clean up

* Add loading spinner

* Add todos

* Fix conflicts

* Move native code into its own folder

* Add peek to installer

* Fix building peek and peekui projects

* Replace UWP namespaces to WinAppSDK

* Working WASDK placeholder project

* Add exit when powertoys runner exit

* Working winui3 with image display

* Add WIC project with <TreatWarningAsErros> false for now

* Fit content to window

* Use Size from Windows.Foundation

* Change order

* Add some todos

* Refactored native/interop code and added helpers to imagepreviewer

* Rename projects

* Move some code

* Remove using

Co-authored-by: Michael Salmon <miksalmon@users.noreply.github.com>
Co-authored-by: Michael Salmon 🐟 <michaelpsalmon@outlook.com>
Co-authored-by: Alireza Ebadi Ghajari <alirezae@microsoft.com>
Co-authored-by: Jessie Su <Jessie.Su@microsoft.com>
Co-authored-by: sujessie <102062556+sujessie@users.noreply.github.com>

* Bump Microsoft.Windows.SDK.BuildTools version

* [Peek] Plugin pattern to enable any file type previewing (#22475)

* [Peek] Fetching image size through PropertyStore (#22530)

* Fetching metadata from PropertySTore

* Releasing objects to fix crash

* Creating new PropertyHelper

Co-authored-by: Daniel Chau <dancha@microsoft.com>

* Juliata/filetypes (#22538)

* Using the same list of file extensions as Lightbox's AppxManifest, and ensuring we convert file extension to lowercase

* Add IsFileTypeSupported to IPreviewer

* respond to PR comments

* Add scale awareness to window centering (#22541)

* [Peek] Fix installer builds, project configs and update assets (#22540)

* Update installer

* Fix installer errors

* Fix peek vcxproj

* Add package signing

* Add peek to arm64

* Add back ARM64 toMeasureToolUI

* Add versions to project

* Update assets and icons

* Add correct icon

* [Peek] Enable PropertyStore for offline files (#22567)

* Enabling PropertyStore for offline files

Co-authored-by: Daniel Chau <dancha@microsoft.com>

* [Peek] Adding unsupported file previewer (#22598)

* Unsupported file previewer

* Fix file display info

* Fix property store calls

* Update TODO

* [Peek] Add WebView2 integration (#22506)

* First commit with WIP logic to support WV2 in Peek module

* Minor code cleanup and try/catch block

* Added control to wrap WebView2 logic

* Cleanup

* Added logic to handle HTML previewing
Properly update FilePreview according to file type

* Code cleanup
Updated comments

* Updated comment

* Removed comment

* Code cleanup

* Improved opening of web browser preview to avoid "blank" or "seeing previous page" issue
Removed unused method
Added xaml fallback to guarantee default/starting state

* Removed folder

* Updated factory logic to match master

* address code review

* addressed PR review

* address PR review

* Address PR review

* address PR review

* Address PR review

* [Peek] Add basic file querying and navigation (#22589)

* Refactor to facilitate file data initialization

* Extract file-related code to new FileManager class

* Add temp basic version

* Clean + add todo for cancellations

* Fix various nav-related issues

* Temp - start moving iteration-related code to bg thread

* Minor tweaks

* Add FEHelper todo

* Rename FileManager + various tweaks

* Add basic throttling

* Improve bg thread synchronization

* Clean

* Clean

* Rename based on feedback

* Rename FileQuery

* Rename properties

* Rename remaining fields

* Add todos for nav success/failures

Co-authored-by: Esteban Margaron <emargaron@microsoft.com>

* [Peek] Add customized title bar  (#22600)

* Add basic button UI

* Add function to get default app name and to open file in default app

* Correct error output

* Add filename to titlebar

* Remove titlebar text from Resw

* Add basic button UI

* Add function to get default app name and to open file in default app

* Add filename to titlebar

* Correct error output

* Remove titlebar text from Resw

* Add SetDragRectangles

* Correct logic, update function name

* Add localization

* Cleanup and adaptive width

* Add fileIndex/NumberOfFiles for multiple files activation

* Refine titlebar styles

* Update error message; Return HResult from native methods; Update variable initialisation and string null testing

* Titlebar height and adaptive width refinement

* Add fallback to launch app picker if fail to open default app

* Temp change to hide AppTitle_FileCount

* Update launch button to command; Add keyboard accelerator

* Update titlebar inactive background color

* Update tooltip to add keyboard accelerator

* Add comments to resw file

* Fix accidental deletion from previous merge

Co-authored-by: Jojo Zhou <yizzho@microsoft.com>
Co-authored-by: Yawen Hou <yawenhou@microsoft.com>

* Fix crash

* Fix wrong thread exception

* Make CurrentItemIndex setter private

* Update titlebar filecount text

* Fix titlebar draggable region and interactive region (bump WinAppSdk to latest)

* [Peek] Unsupported File Previewer - Formatting string from resources (#22609)

* Moving to string resource usage

* Moving ReadableStringHelper to common project

* Fix comments

* [Peek] Fix foregrounding (#22633)

* Fixing foregrounding

* Get window handle inside BringToForeground extension method

Co-authored-by: Daniel Chau <dancha@microsoft.com>
Co-authored-by: Samuel Chapleau <sachaple@microsoft.com>

* [Peek] ImagePreviewer - Handle error states (#22637)

* add better preview state handling

* add error handling in imagepreviewer and better state handling

* fix error handling so exception is not bubbled up

* improve performance and hook up unsupported previewer on error

* remove commented code

* address pr comments

* [Peek] add PDF viewing support (#22636)

* [Peek] add PDF viewing support

* Fixed issue which would redirect some HTML and PDF files to external browser

* Fixed refactored interface name

* [Peek] Refine titlebar adaptive width (#22642)

* Adjust adaptive width of titlebar

* Remove visualstate setters for AppTitle_FileCount

Co-authored-by: Jojo Zhou <yizzho@microsoft.com>

* [Peek] New File Explorer tabs break Shell API to get selected files (#22641)

* fix FE tab bug

* remove unnecessary unsafe keyword

* [Peek] add extra logic to properly render PNG files with transparency (#22613)

* [Peek] added extra logic to render PNG files with proper transparency

* Moved logic to ThumbnailHelper
Cleanup

* Created a separated previewer for PNG to only load the preview image with thumbnail logic

* removed unused code

* Updated state loading change

* [Peek] Unsupported File Previewer - Setting Window Size (#22645)

* Adding setting for unsupported file window

* Fix

* [Peek] Add tooltip to File (#22640)

* Add tooltip to File

* Add placeholder text for no tooltip

* Address comments

* Use StringBuilder

Co-authored-by: Jojo Zhou <yizzho@microsoft.com>

* Add full image quality support (#22654)

* [Peek] Window foregrounding simplification and fixes + keep window visible if FE single selection changed (#22657)

* Use different apis to bring to foreground removing remote thread wait and work as well as library loading

* Keep window open if single selected file in FE is different

* Removed unused methods

* [Peek] Add cancellation token OnFilePropertyChanged (#22643)

* Cancel file loading before opening another file

* Add omitted cancellation checks

* Catch task cancelled exception; Add more cancellation checkpoints

* Add cancellation checkpoint beofre GetBitmapFromHBitmapAsync

* Correct typo

* Update to pass cancellation token individually to each async methods

* Add lost cancellationToken source

* Add cancellation token to PngPreviewer

Co-authored-by: Yawen Hou <yawenhou@microsoft.com>

* [Peek] Unsupported File Previewer - Preserve Transparency For File Icons (#22650)

* Preserving transparency or icons

* Remove TODO

Co-authored-by: Samuel Chapleau <sachaple@microsoft.com>

* [Peek] Update some installer build steps + assets update (#22683)

* Fix settings & peek.ui.wpf

* Add back missing icon

* Add missing files and actions to installer

* Keep window open if the selected file in explorer is different (only works for single file selection)

* Undo last

* [Peek] Add copy keyboard accelerator (#22647)

* add copy keyboard accelerator

* Fix comments

Co-authored-by: Samuel Chapleau <sachaple@microsoft.com>

* [Peek] add WV2 improvements (behavior and UX) (#22685)

* [Peek] added logic to get max monitor size for opening WebView2

* Removed ununsed dependency property

* Added workaround for cases where the web page would not finish navigating in a quick timing, for example google.com.

* Remove window extensions from common and use nullable size argument instead

Co-authored-by: Samuel Chapleau <sachaple@microsoft.com>

* [Peek] Merge main, self-contained .NET and fix WebView2 user data dir issue (#22899)

* Merge remote-tracking branch 'origin/main' into peek

* Test sc

* Set WebView2 user data dir

* spellcheck

* Fix comment

* Move check if higher quality image is already loaded to the exact line where we change the Preview bitmap (#23083)

* Fix opening Peek when FE window is set to full name path (#23082)

* Move check for png thubmnail loading priority

* Remove Peek.UI.WPF project

* Remove duplicated method in powertoys setup

* [Peek] Fix selecting files from the correct focused opened File Explorer tab & from Desktop (#23489)

* Get file based on active tab handle instead of window title

* Refactor code to get active tab

* Getting all items from the shell API working again, except for desktop

* Refactor and cleanup com & native code

* Add back removed peek xaml assets in Product.wxs

* Remove some dependencies that do not seem necessary in Product.wxs

* [Peek] Small images (#23554)

* change stretch value

* compare with actual window size

* consider scaling factor

* set max size

* clean up

* clean up

* clean up previewers

* scaling factor in bitmap previewer

* max image size property

* [Peek]Handle errors for HEIC/HEIF and fall back to default previewer if there is no thumbnail (#22684)

* Handle errors when getting filesize by falling back to default previewer

* Bringing back other file types that are fixed with these code changes

---------

Co-authored-by: Samuel Chapleau <sachaple@microsoft.com>

* [Peek] Add unsupported file icon fallback (#23735)

* Refactor icon retrieval, refactor hbitmap to bitmap conversion, add icon fallback

* Add svg to assets in installer

* [Peek] Refactoring of file system models, removal of PngPreviewer, retrieving of folder size via Scripting com reference and other fixes (#23955)

* Refactor icon retrieval, refactor hbitmap to bitmap conversion, add icon fallback

* Add svg to assets in installer

* - Refactor File class into IFileSystemItem, FileItem & FolderItem
- Display size for folders using Scripting namespace
- Remove default app buttons for files or folders not supporting it

* Add better content type via storage apis

* Add check for storagefile in PngPreviewer

* Fix png stretching

* Remove png previewer

* Rename ThumbnailOptions.None to ThumbnailOptions.ResizeToFit

* [Peek] Removed monitor percentage evaluation for the UnsupportedFilePreview control (#24002)

* Remove settings for percentage of windows and keep default min size.

* Fix margin on unsupported control

* Use nullable Size for image size & open file on background thread (#24004)

* [Peek] SVG support (#24237)

* svg previewer

* svg size

* set scaling factor

* set image size

* changed image source type

* non nullable image size

* notify svg previewer changed

* uncomment

* rename BitmapPreviewer

* move svg support

* remove svg previewer

* [Peek] Implementation of a performant and reliable Neighboring Files Query (#24943)

* Use IShellItemArray as the backing array of item

* Finalize and cleanup NFQ implementation

* Cleanup remainder of the code

* Remove unused using

* [Peek] Pin the window position  (#24927)

* [Peek] Telemetry and logging (#25231)

* text preview

* scrolling

* changed size

* webview2 preview

* common preview project

* previewpane: use common project

* peek: use common

* previewpane: moved md

* peek: md

* previewpane: clean up

* clean up

* moved monaco files

* moved formatters

* rename

* moved common monaco helper

* dev files support

* installer

* removed versions

* warnings: culture info

* warnings: names

* clean up

* warnings: dispose

* warnings: default values

* warnings

* warnings: charset

* warnings: exceptions

* suppress warning

* installer: added peek

* changed peek guid

* monaco folders

* peek deps

* peek files

* peek resources

* removed additional monaco folder

* set host name

* Update installer

* hardcode monaco path

* leave single webview control

* moved path to common

* project

* more meaningful todos

* moved temp folder cleanup

* todo

* extension check

* spell: monaco

* spellcheck

* spellcheck

* fix id

* fix spelling

* key to spelling

* id fix

* Fix monaco resolution at install time

* Fix user install. Add needed files

* installer: remove peek localization files. It's a WinUI app

* installer:fix signing

* removed unused

* settings: flyout enable/disable for Peek

* simplify string

* property changed handle

* [Peek][Settings] Peek OOBE page (#25895)

* [Peek] GPO (#25918)

* Add Native methods file to exception

* Fix merge issue on solution file

* Adjust spellcheck

* Remove boilerplate code

* Add module interface telemetry

* Remove change to README.md

* Add entry to README

* Clean up some non-changes

* Fix order of Peek in Settings menu

* [Settings] Make peek descriptions more descriptive

---------

Co-authored-by: Michael Salmon <miksalmon@users.noreply.github.com>
Co-authored-by: Michael Salmon 🐟 <michaelpsalmon@outlook.com>
Co-authored-by: Alireza Ebadi Ghajari <alirezae@microsoft.com>
Co-authored-by: Jessie Su <Jessie.Su@microsoft.com>
Co-authored-by: sujessie <102062556+sujessie@users.noreply.github.com>
Co-authored-by: Daniel Chau <d.chau@alumni.ubc.ca>
Co-authored-by: Daniel Chau <dancha@microsoft.com>
Co-authored-by: jth-ms <73617023+jth-ms@users.noreply.github.com>
Co-authored-by: Robson <rp.pontin@gmail.com>
Co-authored-by: estebanm123 <49930791+estebanm123@users.noreply.github.com>
Co-authored-by: Esteban Margaron <emargaron@microsoft.com>
Co-authored-by: Yawen Hou <Sytta@users.noreply.github.com>
Co-authored-by: Jojo Zhou <yizzho@microsoft.com>
Co-authored-by: Yawen Hou <yawenhou@microsoft.com>
Co-authored-by: Jojo Zhou <39350350+Joanna-Zhou@users.noreply.github.com>
Co-authored-by: Stefan Markovic <57057282+stefansjfw@users.noreply.github.com>
Co-authored-by: Seraphima Zykova <zykovas91@gmail.com>
Co-authored-by: Stefan Markovic <stefan@janeasystems.com>
Co-authored-by: Jaime Bernardo <jaime@janeasystems.com>
This commit is contained in:
Samuel Chapleau
2023-05-10 10:43:03 -07:00
committed by GitHub
parent 7ae93a9c7f
commit 648f30d1ab
361 changed files with 10709 additions and 182 deletions

View File

@@ -0,0 +1,13 @@
// 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;
namespace Peek.Common.Constants
{
public static class TempFolderPath
{
public static string Path => $"{Environment.GetEnvironmentVariable("USERPROFILE")}\\AppData\\LocalLow\\Microsoft\\PowerToys\\Peek-Temp";
}
}

View File

@@ -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.Common.Constants
{
public static class WindowConstants
{
public const double MaxWindowToMonitorRatio = 0.80;
public const double MinWindowHeight = 500;
public const double MinWindowWidth = 680;
public const double WindowWidthContentPadding = 7;
public const double WindowHeightContentPadding = 16;
}
}

View File

@@ -0,0 +1,14 @@
// 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.Converters
{
public static class BoolConverter
{
public static bool Invert(bool value)
{
return !value;
}
}
}

View File

@@ -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;
using System.Threading.Tasks;
using Microsoft.UI.Dispatching;
namespace Peek.Common.Extensions
{
public static class DispatcherExtensions
{
/// <summary>
/// Run work on UI thread safely.
/// </summary>
/// <returns>True if the work was run successfully, False otherwise.</returns>
public static Task RunOnUiThread(this DispatcherQueue dispatcher, Func<Task> work)
{
var tcs = new TaskCompletionSource();
dispatcher.TryEnqueue(async () =>
{
try
{
await work();
tcs.SetResult();
}
catch (Exception e)
{
tcs.SetException(e);
}
});
return tcs.Task;
}
}
}

View File

@@ -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.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;
using Peek.Common.Models;
using Scripting;
using Windows.Foundation;
using Windows.Storage;
namespace Peek.Common.Extensions
{
public static class IFileSystemItemExtensions
{
public static Size? GetImageSize(this IFileSystemItem item)
{
Size? size = null;
var propertyStore = item.PropertyStore;
var width = propertyStore.TryGetUInt(PropertyKey.ImageHorizontalSize);
var height = propertyStore.TryGetUInt(PropertyKey.ImageVerticalSize);
if (width != null && height != null)
{
size = new Size((int)width, (int)height);
}
return size;
}
public static Size? GetSvgSize(this IFileSystemItem item)
{
Size? size = null;
using (FileStream stream = System.IO.File.OpenRead(item.Path))
{
XmlReaderSettings settings = new XmlReaderSettings();
settings.Async = true;
settings.IgnoreComments = true;
settings.IgnoreProcessingInstructions = true;
settings.IgnoreWhitespace = true;
using (XmlReader reader = XmlReader.Create(stream, settings))
{
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element && reader.Name == "svg")
{
string? width = reader.GetAttribute("width");
string? height = reader.GetAttribute("height");
if (width != null && height != null)
{
int widthValue = int.Parse(Regex.Match(width, @"\d+").Value, NumberFormatInfo.InvariantInfo);
int heightValue = int.Parse(Regex.Match(height, @"\d+").Value, NumberFormatInfo.InvariantInfo);
size = new Size(widthValue, heightValue);
}
else
{
string? viewBox = reader.GetAttribute("viewBox");
if (viewBox != null)
{
var viewBoxValues = viewBox.Split(' ');
if (viewBoxValues.Length == 4)
{
int viewBoxWidth = int.Parse(viewBoxValues[2], NumberStyles.Integer, CultureInfo.InvariantCulture);
int viewBoxHeight = int.Parse(viewBoxValues[3], NumberStyles.Integer, CultureInfo.InvariantCulture);
size = new Size(viewBoxWidth, viewBoxHeight);
}
}
}
reader.Close();
}
}
}
}
return size;
}
public static ulong GetSizeInBytes(this IFileSystemItem item)
{
ulong sizeInBytes = 0;
switch (item)
{
case FolderItem _:
FileSystemObject fileSystemObject = new FileSystemObject();
Folder folder = fileSystemObject.GetFolder(item.Path);
sizeInBytes = (ulong)folder.Size;
break;
case FileItem _:
var propertyStore = item.PropertyStore;
sizeInBytes = propertyStore.TryGetULong(PropertyKey.FileSizeBytes) ?? 0;
break;
}
return sizeInBytes;
}
public static async Task<string> GetContentTypeAsync(this IFileSystemItem item)
{
string contentType = string.Empty;
var storageItem = await item.GetStorageItemAsync();
switch (storageItem)
{
case StorageFile storageFile:
contentType = storageFile.DisplayType;
break;
case StorageFolder storageFolder:
contentType = storageFolder.DisplayType;
break;
default:
var propertyStore = item.PropertyStore;
contentType = propertyStore.TryGetString(PropertyKey.FileType) ?? string.Empty;
break;
}
return contentType;
}
}
}

View File

@@ -0,0 +1,161 @@
// 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.Runtime.InteropServices;
using Peek.Common.Models;
namespace Peek.Common.Extensions
{
public static class IPropertyStoreExtensions
{
/// <summary>
/// Helper method that retrieves a uint value from the given property store.
/// Returns 0 if the value is not a VT_UI4 (4-byte unsigned integer in little-endian order).
/// </summary>
/// <param name="propertyStore">The property store</param>
/// <param name="key">The pkey</param>
/// <returns>The uint value</returns>
public static uint? TryGetUInt(this IPropertyStore propertyStore, PropertyKey key)
{
if (propertyStore == null)
{
return null;
}
try
{
PropVariant propVar;
propertyStore.GetValue(ref key, out propVar);
// VT_UI4 Indicates a 4-byte unsigned integer formatted in little-endian byte order.
if ((VarEnum)propVar.Vt == VarEnum.VT_UI4)
{
return propVar.UlVal;
}
else
{
return null;
}
}
catch (Exception)
{
return null;
}
}
/// <summary>
/// Helper method that retrieves a ulong value from the given property store.
/// Returns 0 if the value is not a VT_UI8 (8-byte unsigned integer in little-endian order).
/// </summary>
/// <param name="propertyStore">The property store</param>
/// <param name="key">the pkey</param>
/// <returns>the ulong value</returns>
public static ulong? TryGetULong(this IPropertyStore propertyStore, PropertyKey key)
{
if (propertyStore == null)
{
return null;
}
try
{
PropVariant propVar;
propertyStore.GetValue(ref key, out propVar);
// VT_UI8 Indicates an 8-byte unsigned integer formatted in little-endian byte order.
if ((VarEnum)propVar.Vt == VarEnum.VT_UI8)
{
return propVar.UhVal;
}
else
{
return null;
}
}
catch (Exception)
{
return null;
}
}
/// <summary>
/// Helper method that retrieves a string value from the given property store.
/// </summary>
/// <param name="propertyStore">The property store</param>
/// <param name="key">The pkey</param>
/// <returns>The string value</returns>
public static string? TryGetString(this IPropertyStore propertyStore, PropertyKey key)
{
if (propertyStore == null)
{
return null;
}
try
{
PropVariant propVar;
propertyStore.GetValue(ref key, out propVar);
if ((VarEnum)propVar.Vt == VarEnum.VT_LPWSTR)
{
return Marshal.PtrToStringUni(propVar.P) ?? string.Empty;
}
else
{
return null;
}
}
catch (Exception)
{
return null;
}
}
/// <summary>
/// Helper method that retrieves an array of string values from the given property store.
/// </summary>
/// <param name="propertyStore">The property store</param>
/// <param name="key">The pkey</param>
/// <returns>The array of string values</returns>
public static string[]? TryGetStringArray(this IPropertyStore propertyStore, PropertyKey key)
{
if (propertyStore == null)
{
return null;
}
try
{
PropVariant propVar;
propertyStore.GetValue(ref key, out propVar);
List<string>? values = null;
if ((VarEnum)propVar.Vt == (VarEnum.VT_LPWSTR | VarEnum.VT_VECTOR))
{
values = new List<string>();
for (int elementIndex = 0; elementIndex < propVar.Calpwstr.CElems; elementIndex++)
{
var stringVal = Marshal.PtrToStringUni(Marshal.ReadIntPtr(propVar.Calpwstr.PElems, elementIndex));
if (stringVal != null)
{
values.Add(stringVal);
}
}
}
return values?.ToArray();
}
catch (Exception)
{
return null;
}
}
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading.Tasks;
namespace Peek.Common.Extensions
{
public static class TaskExtension
{
public static Task<bool> RunSafe(Func<Task> work)
{
var tcs = new TaskCompletionSource<bool>();
Task.Run(async () =>
{
try
{
await work();
tcs.SetResult(true);
}
catch (Exception)
{
tcs.SetResult(false);
}
});
return tcs.Task;
}
}
}

View File

@@ -0,0 +1,33 @@
// 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 Windows.ApplicationModel.DataTransfer;
using Windows.Storage;
using Windows.Storage.Streams;
namespace Peek.Common.Helpers
{
public static class ClipboardHelper
{
public static void SaveToClipboard(IStorageItem? storageItem)
{
if (storageItem == null)
{
return;
}
var dataPackage = new DataPackage();
dataPackage.SetStorageItems(new IStorageItem[1] { storageItem }, false);
if (storageItem is StorageFile storageFile)
{
RandomAccessStreamReference imageStreamRef = RandomAccessStreamReference.CreateFromFile(storageFile);
dataPackage.Properties.Thumbnail = imageStreamRef;
dataPackage.SetBitmap(imageStreamRef);
}
Clipboard.SetContent(dataPackage);
}
}
}

View File

@@ -0,0 +1,78 @@
// 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.Diagnostics;
using System.Globalization;
using System.IO;
using Scripting;
namespace Peek.Common.Helpers
{
public static class Logger
{
private static readonly IFileSystem _fileSystem = new FileSystemObject();
private static readonly string ApplicationLogPath = Path.Combine(interop.Constants.AppDataPath(), "Peek\\Logs");
static Logger()
{
if (!_fileSystem.FolderExists(ApplicationLogPath))
{
_fileSystem.CreateFolder(ApplicationLogPath);
}
// Using InvariantCulture since this is used for a log file name
var logFilePath = _fileSystem.BuildPath(ApplicationLogPath, "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".txt");
Trace.Listeners.Add(new TextWriterTraceListener(logFilePath));
Trace.AutoFlush = true;
}
public static void LogError(string message)
{
Log(message, "ERROR");
}
public static void LogError(string message, Exception ex)
{
Log(
message + Environment.NewLine +
ex?.Message + Environment.NewLine +
"Inner exception: " + Environment.NewLine +
ex?.InnerException?.Message + Environment.NewLine +
"Stack trace: " + Environment.NewLine +
ex?.StackTrace,
"ERROR");
}
public static void LogWarning(string message)
{
Log(message, "WARNING");
}
public static void LogInfo(string message)
{
Log(message, "INFO");
}
private static void Log(string message, string type)
{
Trace.WriteLine(type + ": " + DateTime.Now.TimeOfDay);
Trace.Indent();
Trace.WriteLine(GetCallerInfo());
Trace.WriteLine(message);
Trace.Unindent();
}
private static string GetCallerInfo()
{
StackTrace stackTrace = new StackTrace();
var methodName = stackTrace.GetFrame(3)?.GetMethod();
var className = methodName?.DeclaringType?.Name ?? string.Empty;
return "[Method]: " + methodName?.Name + " [Class]: " + className;
}
}
}

View File

@@ -0,0 +1,14 @@
// 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.Helpers
{
public static class MathHelper
{
public static int Modulo(int a, int b)
{
return a < 0 ? ((a % b) + b) % b : a % b;
}
}
}

View File

@@ -0,0 +1,66 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Runtime.InteropServices;
using Peek.Common.Models;
using Windows.Win32.UI.Shell.PropertiesSystem;
namespace Peek.Common.Helpers
{
public static partial class PropertyStoreHelper
{
/// <summary>
/// Gets a IPropertyStore interface from the given path.
/// </summary>
/// <param name="path">The file/folder path</param>
/// <param name="flags">The property store flags</param>
/// <returns>an IPropertyStroe interface</returns>
public static IPropertyStore GetPropertyStoreFromPath(string path, GETPROPERTYSTOREFLAGS flags = GETPROPERTYSTOREFLAGS.GPS_EXTRINSICPROPERTIES)
{
IShellItem2? shellItem2 = null;
IntPtr ppPropertyStore = IntPtr.Zero;
try
{
SHCreateItemFromParsingName(path, IntPtr.Zero, typeof(IShellItem2).GUID, out shellItem2);
if (shellItem2 == null)
{
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Unable to get an IShellItem2 reference from file {0}.", path));
}
int hr = shellItem2.GetPropertyStore((int)flags, typeof(IPropertyStore).GUID, out ppPropertyStore);
if (hr != 0)
{
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "GetPropertyStore returned hresult={0}", hr));
}
return (IPropertyStore)Marshal.GetObjectForIUnknown(ppPropertyStore);
}
finally
{
if (ppPropertyStore != IntPtr.Zero)
{
Marshal.Release(ppPropertyStore);
}
if (shellItem2 != null)
{
Marshal.ReleaseComObject(shellItem2);
}
}
}
[DllImport("shell32.dll", CharSet = CharSet.Unicode, PreserveSig = false)]
private static extern void SHCreateItemFromParsingName(
[In][MarshalAs(UnmanagedType.LPWStr)] string pszPath,
[In] IntPtr pbc,
[In][MarshalAs(UnmanagedType.LPStruct)] Guid riid,
[Out][MarshalAs(UnmanagedType.Interface, IidParameterIndex = 2)] out IShellItem2 ppv);
}
}

View File

@@ -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.
using System;
using System.Collections.Generic;
using System.Globalization;
using Windows.ApplicationModel.Resources;
namespace Peek.Common.Helpers
{
public static class ReadableStringHelper
{
private const int DecimalPercision = 10;
public static string BytesToReadableString(ulong bytes)
{
var resourceLoader = ResourceLoader.GetForViewIndependentUse();
List<string> format = new List<string>
{
resourceLoader.GetString("ReadableString_ByteAbbreviationFormat"), // "B"
resourceLoader.GetString("ReadableString_KiloByteAbbreviationFormat"), // "KB"
resourceLoader.GetString("ReadableString_MegaByteAbbreviationFormat"), // "MB"
resourceLoader.GetString("ReadableString_GigaByteAbbreviationFormat"), // "GB"
resourceLoader.GetString("ReadableString_TeraByteAbbreviationFormat"), // "TB"
resourceLoader.GetString("ReadableString_PetaByteAbbreviationFormat"), // "PB"
resourceLoader.GetString("ReadableString_ExaByteAbbreviationFormat"), // "EB"
};
int index = 0;
double number = 0.0;
if (bytes > 0)
{
index = (int)Math.Floor(Math.Log(bytes) / Math.Log(1024));
number = Math.Round((bytes / Math.Pow(1024, index)) * DecimalPercision) / DecimalPercision;
}
return string.Format(CultureInfo.InvariantCulture, format[index], number);
}
public static string FormatResourceString(string resourceId, object? args)
{
var formatString = ResourceLoader.GetForViewIndependentUse()?.GetString(resourceId);
var formattedString = string.IsNullOrEmpty(formatString) ? string.Empty : string.Format(CultureInfo.InvariantCulture, formatString, args);
return formattedString;
}
public static string FormatResourceString(string resourceId, object? args0, object? args1)
{
var formatString = ResourceLoader.GetForViewIndependentUse()?.GetString(resourceId);
var formattedString = string.IsNullOrEmpty(formatString) ? string.Empty : string.Format(CultureInfo.InvariantCulture, formatString, args0, args1);
return formattedString;
}
}
}

View File

@@ -0,0 +1,53 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading.Tasks;
using Peek.Common.Helpers;
using Windows.Storage;
#nullable enable
namespace Peek.Common.Models
{
public class FileItem : IFileSystemItem
{
private StorageFile? storageFile;
private Lazy<IPropertyStore> _propertyStore;
public FileItem(string path)
{
Path = path;
_propertyStore = new(() => PropertyStoreHelper.GetPropertyStoreFromPath(Path));
}
public string Path { get; init; }
public IPropertyStore PropertyStore => _propertyStore.Value;
public async Task<IStorageItem?> GetStorageItemAsync()
{
return await GetStorageFileAsync();
}
public async Task<StorageFile?> GetStorageFileAsync()
{
if (storageFile == null)
{
try
{
storageFile = await StorageFile.GetFileFromPathAsync(Path);
}
catch (Exception ex)
{
Logger.LogError("Error getting file from path. " + ex.Message);
storageFile = null;
}
}
return storageFile;
}
}
}

View File

@@ -0,0 +1,53 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading.Tasks;
using Peek.Common.Helpers;
using Windows.Storage;
#nullable enable
namespace Peek.Common.Models
{
public class FolderItem : IFileSystemItem
{
private StorageFolder? storageFolder;
private Lazy<IPropertyStore> _propertyStore;
public FolderItem(string path)
{
Path = path;
_propertyStore = new(() => PropertyStoreHelper.GetPropertyStoreFromPath(Path));
}
public string Path { get; init; }
public IPropertyStore PropertyStore => _propertyStore.Value;
public async Task<IStorageItem?> GetStorageItemAsync()
{
return await GetStorageFolderAsync();
}
public async Task<StorageFolder?> GetStorageFolderAsync()
{
if (storageFolder == null)
{
try
{
storageFolder = await StorageFolder.GetFolderFromPathAsync(Path);
}
catch (Exception ex)
{
Logger.LogError("Error getting folder from path. " + ex.Message);
storageFolder = null;
}
}
return storageFolder;
}
}
}

View File

@@ -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.Globalization;
using System.Threading.Tasks;
using Peek.Common.Helpers;
using Windows.Storage;
#nullable enable
namespace Peek.Common.Models
{
public interface IFileSystemItem
{
public DateTime DateModified => System.IO.File.GetCreationTime(Path);
public string Extension => System.IO.Path.GetExtension(Path).ToLower(CultureInfo.InvariantCulture);
public string Name => System.IO.Path.GetFileName(Path);
public string Path { get; init; }
public IPropertyStore PropertyStore { get; }
public Task<IStorageItem?> GetStorageItemAsync();
}
}

View File

@@ -0,0 +1,16 @@
// 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.Common.Models
{
[StructLayout(LayoutKind.Sequential)]
public struct Blob
{
public int CbSize;
public IntPtr PBlobData;
}
}

View File

@@ -0,0 +1,16 @@
// 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.Common.Models
{
[StructLayout(LayoutKind.Sequential)]
public struct CALPWSTR
{
public uint CElems;
public IntPtr PElems;
}
}

View File

@@ -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.
using System.Runtime.InteropServices;
namespace Peek.Common.Models
{
[StructLayout(LayoutKind.Sequential)]
public struct FileTime
{
public int DWHighDateTime;
public int DWLowDateTime;
}
}

View File

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

View File

@@ -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.Runtime.InteropServices;
namespace Peek.Common.Models
{
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("000214F2-0000-0000-C000-000000000046")]
public interface IEnumIDList
{
[PreserveSig]
#pragma warning disable CA1716
int Next(int celt, out IntPtr rgelt, out int pceltFetched);
#pragma warning restore CA1716
[PreserveSig]
int Skip(int celt);
[PreserveSig]
int Reset();
[PreserveSig]
int Clone(out IEnumIDList ppenum);
}
}

View File

@@ -0,0 +1,46 @@
// 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;
using System.Runtime.InteropServices;
using System.Security;
namespace Peek.Common.Models
{
[ComImport]
[Guid("cde725b0-ccc9-4519-917e-325d72fab4ce")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[SuppressUnmanagedCodeSecurity]
public interface IFolderView
{
void GetCurrentViewMode([Out] out uint pViewMode);
void SetCurrentViewMode([In] uint viewMode);
void GetFolder([In, MarshalAs(UnmanagedType.LPStruct)] Guid riid, [Out, MarshalAs(UnmanagedType.Interface)] out object ppv);
void Item([In] int iItemIndex, [Out] out IntPtr ppidl);
void ItemCount([In] uint uFlags, [Out] out int pcItems);
void Items([In] uint uFlags, [In, MarshalAs(UnmanagedType.LPStruct)] Guid riid, [Out, MarshalAs(UnmanagedType.Interface)] out object ppv);
void GetSelectionMarkedItem([Out] out int piItem);
void GetFocusedItem([Out] out int piItem);
void GetItemPosition([In] IntPtr pidl, [Out] out Point ppt);
void GetSpacing([In, Out] ref Point ppt);
void GetDefaultSpacing([Out] out Point ppt);
void GetAutoArrange();
void SelectItem([In] int iItem, [In] uint dwFlags);
void SelectAndPositionItems([In] uint cidl, [In] IntPtr apidl, [In] IntPtr apt, [In] uint dwFlags);
}
}

View File

@@ -0,0 +1,26 @@
// 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.Common.Helpers.PropertyStoreHelper;
namespace Peek.Common.Models
{
[ComImport]
[Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IPropertyStore
{
void GetCount(out uint propertyCount);
void GetAt(uint iProp, out PropertyKey pkey);
void GetValue(ref PropertyKey key, out PropVariant pv);
void SetValue(ref PropertyKey key, ref PropVariant pv);
void Commit();
}
}

View File

@@ -0,0 +1,17 @@
// 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.Common.Models
{
[Guid("6D5140C1-7436-11CE-8034-00AA006009FA")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IServiceProvider
{
[return: MarshalAs(UnmanagedType.IUnknown)]
object QueryService([MarshalAs(UnmanagedType.LPStruct)] Guid service, [MarshalAs(UnmanagedType.LPStruct)] Guid riid);
}
}

View File

@@ -0,0 +1,46 @@
// 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.Common.Models
{
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("000214E2-0000-0000-C000-000000000046")]
public interface IShellBrowser
{
void GetWindow(out IntPtr phwnd);
void ContextSensitiveHelp(bool fEnterMode);
void InsertMenusSB(IntPtr hmenuShared, IntPtr lpMenuWidths);
void SetMenuSB(IntPtr hmenuShared, IntPtr holeMenuRes, IntPtr hwndActiveObject);
void RemoveMenusSB(IntPtr hmenuShared);
void SetStatusTextSB(IntPtr pszStatusText);
void EnableModelessSB(bool fEnable);
void TranslateAcceleratorSB(IntPtr pmsg, ushort wID);
void BrowseObject(IntPtr pidl, uint wFlags);
void GetViewStateStream(uint grfMode, IntPtr ppStrm);
void GetControlWindow(uint id, out IntPtr lpIntPtr);
void SendControlMsg(uint id, uint uMsg, uint wParam, uint lParam, IntPtr pret);
[return: MarshalAs(UnmanagedType.IUnknown)]
object QueryActiveShellView();
void OnViewWindowActive(IShellView ppshv);
void SetToolbarItems(IntPtr lpButtons, uint nButtons, uint uFlags);
}
}

View File

@@ -0,0 +1,67 @@
// 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 Windows.Win32.UI.Shell;
namespace Peek.Common.Models
{
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("93F2F68C-1D1B-11D3-A30E-00C04F79ABD1")]
public interface IShellFolder2
{
[PreserveSig]
int ParseDisplayName(IntPtr hwnd, IntPtr pbc, [MarshalAs(UnmanagedType.LPWStr)] string pszDisplayName, ref int pchEaten, out IntPtr ppidl, ref int pdwAttributes);
[PreserveSig]
int EnumObjects(IntPtr hwnd, _SHCONTF grfFlags, out IntPtr enumIDList);
[PreserveSig]
int BindToObject(IntPtr pidl, IntPtr pbc, ref Guid riid, out IntPtr ppv);
[PreserveSig]
int BindToStorage(IntPtr pidl, IntPtr pbc, ref Guid riid, out IntPtr ppv);
[PreserveSig]
int CompareIDs(IntPtr lParam, IntPtr pidl1, IntPtr pidl2);
[PreserveSig]
int CreateViewObject(IntPtr hwndOwner, Guid riid, out IntPtr ppv);
[PreserveSig]
int GetAttributesOf(int cidl, [MarshalAs(UnmanagedType.LPArray)] IntPtr[] apidl, ref IntPtr rgfInOut);
[PreserveSig]
int GetUIObjectOf(IntPtr hwndOwner, int cidl, [MarshalAs(UnmanagedType.LPArray)] IntPtr[] apidl, ref Guid riid, IntPtr rgfReserved, out IntPtr ppv);
[PreserveSig]
int GetDisplayNameOf(IntPtr pidl, SHGDNF uFlags, out Strret lpName);
[PreserveSig]
int SetNameOf(IntPtr hwnd, IntPtr pidl, [MarshalAs(UnmanagedType.LPWStr)] string pszName, int uFlags, out IntPtr ppidlOut);
[PreserveSig]
int EnumSearches(out IntPtr ppenum);
[PreserveSig]
int GetDefaultColumn(int dwReserved, ref IntPtr pSort, out IntPtr pDisplay);
[PreserveSig]
int GetDefaultColumnState(int iColumn, out IntPtr pcsFlags);
[PreserveSig]
int GetDefaultSearchGUID(out IntPtr pguid);
[PreserveSig]
int GetDetailsEx(IntPtr pidl, IntPtr pscid, out IntPtr pv);
[PreserveSig]
int GetDetailsOf(IntPtr pidl, int iColumn, ref SHELLDETAILS psd);
[PreserveSig]
int MapColumnToSCID(int iColumn, IntPtr pscid);
}
}

View File

@@ -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.
using System;
using System.Runtime.InteropServices;
using Windows.Win32.UI.Shell;
namespace Peek.Common.Models
{
[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);
[return: MarshalAs(UnmanagedType.LPWStr)]
string GetDisplayName(SIGDN sigdnName);
void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
void Compare(IShellItem psi, uint hint, out int piOrder);
}
}

View File

@@ -0,0 +1,71 @@
// 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.CompilerServices;
using System.Runtime.InteropServices;
using Windows.Win32.UI.Shell.PropertiesSystem;
namespace Peek.Common.Models
{
[ComImport]
[Guid("7E9FB0D3-919F-4307-AB2E-9B1860310C93")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IShellItem2 : IShellItem
{
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
public void BindToHandler(IntPtr pbc, [In] ref Guid bhid, [In] ref Guid riid, out IntPtr ppv);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
new void GetParent([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
public int GetDisplayName([In] int sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
public void GetAttributes([In] int sfgaoMask, out int psfgaoAttribs);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
new void Compare([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, [In] uint hint, out int piOrder);
[PreserveSig]
public int GetPropertyStore(int flags, ref Guid riid, out IntPtr ppv);
[PreserveSig]
internal int GetPropertyStoreWithCreateObject(ref GETPROPERTYSTOREFLAGS flags, ref IntPtr punkFactory, ref Guid riid, out IntPtr ppv);
[PreserveSig]
internal int GetPropertyStoreForKeys(ref PropertyKey keys, uint cKeys, ref GETPROPERTYSTOREFLAGS flags, ref Guid riid, out IntPtr ppv);
[PreserveSig]
public int GetPropertyDescriptionList(ref PropertyKey key, ref Guid riid, out IntPtr ppv);
[PreserveSig]
public int Update(ref IntPtr pbc);
[PreserveSig]
public int GetProperty(ref PropertyKey key, out PropVariant pPropVar);
[PreserveSig]
public int GetCLSID(ref PropertyKey key, out Guid clsid);
[PreserveSig]
public int GetFileTime(ref PropertyKey key, out FileTime pft);
[PreserveSig]
public int GetInt32(ref PropertyKey key, out int pi);
[PreserveSig]
public int GetString(ref PropertyKey key, [MarshalAs(UnmanagedType.LPWStr)] string ppsz);
[PreserveSig]
public int GetUint32(ref PropertyKey key, out uint pui);
[PreserveSig]
public int GetUint64(ref PropertyKey key, out uint pull);
[PreserveSig]
public int GetBool(ref PropertyKey key, bool pf);
}
}

View File

@@ -0,0 +1,38 @@
// 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.CompilerServices;
using System.Runtime.InteropServices;
using Windows.Win32.UI.Shell;
namespace Peek.Common.Models
{
[ComImport]
[Guid("B63EA76D-1F85-456F-A19C-48159EFA858B")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IShellItemArray
{
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void BindToHandler([In, MarshalAs(UnmanagedType.Interface)] IntPtr pbc, [In] ref Guid rbhid, [In] ref Guid riid, out IntPtr ppvOut);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void GetPropertyStore([In] int flags, [In] ref Guid riid, out IntPtr ppv);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void GetPropertyDescriptionList([In] ref PropertyKey keyType, [In] ref Guid riid, out IntPtr ppv);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void GetAttributes([In] SIATTRIBFLAGS dwAttribFlags, [In] uint sfgaoMask, out uint psfgaoAttribs);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
int GetCount();
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
Common.Models.IShellItem GetItemAt(int dwIndex);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void EnumItems([MarshalAs(UnmanagedType.Interface)] out IntPtr ppenumShellItems);
}
}

View File

@@ -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.
using System;
using System.Runtime.InteropServices;
namespace Peek.Common.Models
{
[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
{
ResizeToFit = 0x00,
BiggerSizeOk = 0x01,
InMemoryOnly = 0x02,
IconOnly = 0x04,
ThumbnailOnly = 0x08,
InCacheOnly = 0x10,
ScaleUp = 0x100,
}
}

View File

@@ -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.
using System;
using System.Runtime.InteropServices;
using System.Security;
namespace Peek.Common.Models
{
[ComImport]
[Guid("000214E3-0000-0000-C000-000000000046")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[SuppressUnmanagedCodeSecurity]
public interface IShellView
{
}
}

View File

@@ -0,0 +1,60 @@
// 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.Common.Models
{
[StructLayout(LayoutKind.Explicit)]
public struct PropVariant
{
[FieldOffset(0)]
public short Vt;
[FieldOffset(2)]
public short WReserved1;
[FieldOffset(4)]
public short WReserved2;
[FieldOffset(6)]
public short WReserved3;
[FieldOffset(8)]
public sbyte CVal;
[FieldOffset(8)]
public byte BVal;
[FieldOffset(8)]
public short IVal;
[FieldOffset(8)]
public ushort UiVal;
[FieldOffset(8)]
public int LVal;
[FieldOffset(8)]
public uint UlVal;
[FieldOffset(8)]
public int IntVal;
[FieldOffset(8)]
public uint UintVal;
[FieldOffset(8)]
public long HVal;
[FieldOffset(8)]
public ulong UhVal;
[FieldOffset(8)]
public float FltVal;
[FieldOffset(8)]
public double DblVal;
[FieldOffset(8)]
public bool BoolVal;
[FieldOffset(8)]
public int Scode;
[FieldOffset(8)]
public DateTime Date;
[FieldOffset(8)]
public FileTime Filetime;
[FieldOffset(8)]
public Blob Blob;
[FieldOffset(8)]
public IntPtr P;
[FieldOffset(8)]
public CALPWSTR Calpwstr;
}
}

View File

@@ -0,0 +1,65 @@
// 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.Common.Models
{
[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct PropertyKey
{
public Guid FormatId;
public int PropertyId;
public PropertyKey(Guid keyGuid, int propertyId)
{
this.FormatId = keyGuid;
this.PropertyId = propertyId;
}
public PropertyKey(uint a, uint b, uint c, uint d, uint e, uint f, uint g, uint h, uint i, uint j, uint k, int propertyId)
: this(new Guid((uint)a, (ushort)b, (ushort)c, (byte)d, (byte)e, (byte)f, (byte)g, (byte)h, (byte)i, (byte)j, (byte)k), propertyId)
{
}
public override bool Equals(object? obj)
{
if ((obj == null) || !(obj is PropertyKey))
{
return false;
}
PropertyKey pk = (PropertyKey)obj;
return FormatId.Equals(pk.FormatId) && (PropertyId == pk.PropertyId);
}
public static bool operator ==(PropertyKey a, PropertyKey b)
{
if (((object)a == null) || ((object)b == null))
{
return false;
}
return a.FormatId == b.FormatId && a.PropertyId == b.PropertyId;
}
public static bool operator !=(PropertyKey a, PropertyKey b)
{
return !(a == b);
}
public override int GetHashCode()
{
return FormatId.GetHashCode() ^ PropertyId;
}
// File properties: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-wsp/2dbe759c-c955-4770-a545-e46d7f6332ed
public static readonly PropertyKey ImageHorizontalSize = new PropertyKey(new Guid(0x6444048F, 0x4C8B, 0x11D1, 0x8B, 0x70, 0x08, 0x00, 0x36, 0xB1, 0x1A, 0x03), 3);
public static readonly PropertyKey ImageVerticalSize = new PropertyKey(new Guid(0x6444048F, 0x4C8B, 0x11D1, 0x8B, 0x70, 0x08, 0x00, 0x36, 0xB1, 0x1A, 0x03), 4);
public static readonly PropertyKey FileSizeBytes = new PropertyKey(new Guid(0xb725f130, 0x47ef, 0x101a, 0xa5, 0xf1, 0x02, 0x60, 0x8c, 0x9e, 0xeb, 0xac), 12);
public static readonly PropertyKey FileType = new PropertyKey(new Guid(0xd5cdd502, 0x2e9c, 0x101b, 0x93, 0x97, 0x08, 0x00, 0x2b, 0x2c, 0xf9, 0xae), 26);
}
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static Peek.Common.Helpers.PropertyStoreHelper;
namespace Peek.Common.Models
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct SHELLDETAILS
{
public int Fmt;
public int CxChar;
public Strret Str;
}
}

View File

@@ -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.Runtime.InteropServices;
namespace Peek.Common.Models
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct SHFILEINFO
{
public IntPtr HIcon;
public int IIcon;
public uint DwAttributes;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string SzDisplayName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
public string SzTypeName;
}
}

View File

@@ -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.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
namespace Peek.Common.Models
{
[SuppressMessage("Microsoft.Portability", "CA1900:ValueTypeFieldsShouldBePortable", Justification = "Targeting Windows (X86/AMD64/ARM) only")]
[StructLayout(LayoutKind.Explicit)]
public struct Strret
{
[FieldOffset(0)]
public int UType;
[FieldOffset(4)]
public IntPtr POleStr;
[FieldOffset(4)]
public IntPtr PStr;
[FieldOffset(4)]
public int UOffset;
[FieldOffset(4)]
public IntPtr CStr;
}
}

View File

@@ -0,0 +1,4 @@
{
"$schema": "https://aka.ms/CsWin32.schema.json",
"public": true
}

View File

@@ -0,0 +1,5 @@
GETPROPERTYSTOREFLAGS
_SHCONTF
SIGDN
SHGDNF
SIATTRIBFLAGS

View File

@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Version.props" />
<PropertyGroup>
<TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
<TargetPlatformMinVersion>10.0.19041.0</TargetPlatformMinVersion>
<RootNamespace>Peek.Common</RootNamespace>
<RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<COMReference Include="Scripting">
<WrapperTool>tlbimp</WrapperTool>
<VersionMinor>0</VersionMinor>
<VersionMajor>1</VersionMajor>
<Guid>420b2830-e718-11cf-893d-00a0c9054228</Guid>
<Lcid>0</Lcid>
<Isolated>false</Isolated>
<EmbedInteropTypes>true</EmbedInteropTypes>
</COMReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
</ItemGroup>
</Project>

View File

@@ -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.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace Peek.UI.Telemetry.Events
{
[EventData]
public class ClosedEvent : EventBase, IEvent
{
public ClosedEvent()
{
EventName = "Peek_Closed";
}
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}
}

View File

@@ -0,0 +1,35 @@
// 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.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
using Peek.Common.Models;
namespace Peek.UI.Telemetry.Events
{
[EventData]
public class ErrorEvent : EventBase, IEvent
{
public class FailureType
{
public static readonly string PreviewFail = "Preview fail, cannot render file";
public static readonly string FileNotSupported = "Default view shown due to file not supported";
public static readonly string AppCrash = "App crash";
}
public ErrorEvent()
{
EventName = "Peek_Error";
}
public HResult HResult { get; set; }
public string Message { get; set; } = string.Empty;
public string Failure { get; set; } = string.Empty;
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}
}

View File

@@ -0,0 +1,23 @@
// 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.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace Peek.UI.Telemetry.Events
{
[EventData]
public class OpenWithEvent : EventBase, IEvent
{
public OpenWithEvent()
{
EventName = "Peek_OpenWith";
}
public string App { get; set; } = string.Empty;
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}
}

View File

@@ -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.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace Peek.UI.Telemetry.Events
{
[EventData]
public class OpenedEvent : EventBase, IEvent
{
public OpenedEvent()
{
EventName = "Peek_Opened";
}
public string ActivationKind { get; set; } = string.Empty;
public string FileExtension { get; set; } = string.Empty;
public bool IsAppToggledOn { get; set; }
public double HotKeyToVisibleTimeMs { get; set; }
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}
}

View File

@@ -0,0 +1,20 @@
<!-- Copyright (c) Microsoft Corporation. All rights reserved. -->
<!-- Licensed under the MIT License. See LICENSE in the project root for license information. -->
<UserControl
x:Class="Peek.FilePreviewer.Controls.BrowserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Peek.FilePreviewer.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="using:Microsoft.UI.Xaml.Controls"
mc:Ignorable="d">
<Grid>
<controls:WebView2 x:Name="PreviewBrowser"
Loaded="PreviewWV2_Loaded"
NavigationStarting="PreviewBrowser_NavigationStarting"
NavigationCompleted="PreviewWV2_NavigationCompleted"/>
</Grid>
</UserControl>

View File

@@ -0,0 +1,175 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Web.WebView2.Core;
using Peek.Common.Constants;
using Peek.Common.Helpers;
using Windows.System;
namespace Peek.FilePreviewer.Controls
{
public sealed partial class BrowserControl : UserControl, IDisposable
{
/// <summary>
/// Helper private Uri where we cache the last navigated page
/// so we can redirect internal PDF or Webpage links to external
/// web browser, avoiding WebView internal navigation.
/// </summary>
private Uri? _navigatedUri;
public delegate void NavigationCompletedHandler(WebView2? sender, CoreWebView2NavigationCompletedEventArgs? args);
public delegate void DOMContentLoadedHandler(CoreWebView2? sender, CoreWebView2DOMContentLoadedEventArgs? args);
public event NavigationCompletedHandler? NavigationCompleted;
public event DOMContentLoadedHandler? DOMContentLoaded;
public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(
nameof(Source),
typeof(Uri),
typeof(BrowserControl),
new PropertyMetadata(null, new PropertyChangedCallback((d, e) => ((BrowserControl)d).SourcePropertyChanged())));
public Uri? Source
{
get { return (Uri)GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); }
}
public static readonly DependencyProperty IsDevFilePreviewProperty = DependencyProperty.Register(
nameof(IsDevFilePreview),
typeof(bool),
typeof(BrowserControl),
new PropertyMetadata(null, new PropertyChangedCallback((d, e) => ((BrowserControl)d).OnIsDevFilePreviewChanged())));
public bool IsDevFilePreview
{
get
{
return (bool)GetValue(IsDevFilePreviewProperty);
}
set
{
SetValue(IsDevFilePreviewProperty, value);
}
}
public BrowserControl()
{
this.InitializeComponent();
Environment.SetEnvironmentVariable("WEBVIEW2_USER_DATA_FOLDER", TempFolderPath.Path, EnvironmentVariableTarget.Process);
}
public void Dispose()
{
if (PreviewBrowser.CoreWebView2 != null)
{
PreviewBrowser.CoreWebView2.DOMContentLoaded -= CoreWebView2_DOMContentLoaded;
}
Microsoft.PowerToys.FilePreviewCommon.Helper.CleanupTempDir(TempFolderPath.Path);
}
/// <summary>
/// Navigate to the to the <see cref="Uri"/> set in <see cref="Source"/>.
/// Calling <see cref="Navigate"/> will always trigger a navigation/refresh
/// even if web target file is the same.
/// </summary>
public void Navigate()
{
var value = Environment.GetEnvironmentVariable("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS");
_navigatedUri = null;
if (Source != null && PreviewBrowser.CoreWebView2 != null)
{
/* CoreWebView2.Navigate() will always trigger a navigation even if the content/URI is the same.
* Use WebView2.Source to avoid re-navigating to the same content. */
PreviewBrowser.CoreWebView2.Navigate(Source.ToString());
}
}
private void SourcePropertyChanged()
{
Navigate();
}
private void OnIsDevFilePreviewChanged()
{
if (PreviewBrowser.CoreWebView2 != null)
{
PreviewBrowser.CoreWebView2.Settings.IsScriptEnabled = IsDevFilePreview;
if (IsDevFilePreview)
{
PreviewBrowser.CoreWebView2.SetVirtualHostNameToFolderMapping(Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.VirtualHostName, Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.MonacoDirectory, CoreWebView2HostResourceAccessKind.Allow);
}
}
}
private async void PreviewWV2_Loaded(object sender, RoutedEventArgs e)
{
try
{
await PreviewBrowser.EnsureCoreWebView2Async();
PreviewBrowser.CoreWebView2.Settings.AreDefaultScriptDialogsEnabled = false;
PreviewBrowser.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false;
PreviewBrowser.CoreWebView2.Settings.AreDevToolsEnabled = false;
PreviewBrowser.CoreWebView2.Settings.AreHostObjectsAllowed = false;
PreviewBrowser.CoreWebView2.Settings.IsGeneralAutofillEnabled = false;
PreviewBrowser.CoreWebView2.Settings.IsPasswordAutosaveEnabled = false;
PreviewBrowser.CoreWebView2.Settings.IsScriptEnabled = IsDevFilePreview;
PreviewBrowser.CoreWebView2.Settings.IsWebMessageEnabled = false;
if (IsDevFilePreview)
{
PreviewBrowser.CoreWebView2.SetVirtualHostNameToFolderMapping(Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.VirtualHostName, Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.MonacoDirectory, CoreWebView2HostResourceAccessKind.Allow);
}
PreviewBrowser.CoreWebView2.DOMContentLoaded += CoreWebView2_DOMContentLoaded;
}
catch (Exception ex)
{
Logger.LogError("WebView2 loading failed. " + ex.Message);
}
Navigate();
}
private void CoreWebView2_DOMContentLoaded(CoreWebView2 sender, CoreWebView2DOMContentLoadedEventArgs args)
{
DOMContentLoaded?.Invoke(sender, args);
}
private async void PreviewBrowser_NavigationStarting(WebView2 sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationStartingEventArgs args)
{
if (_navigatedUri == null)
{
return;
}
// In case user starts or tries to navigate from within the HTML file we launch default web browser for navigation.
if (args.Uri != null && args.Uri != _navigatedUri?.ToString() && args.IsUserInitiated)
{
args.Cancel = true;
await Launcher.LaunchUriAsync(new Uri(args.Uri));
}
}
private void PreviewWV2_NavigationCompleted(WebView2 sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationCompletedEventArgs args)
{
if (args.IsSuccess)
{
_navigatedUri = Source;
}
NavigationCompleted?.Invoke(sender, args);
}
}
}

View File

@@ -0,0 +1,44 @@
<!-- Copyright (c) Microsoft Corporation and Contributors. -->
<!-- Licensed under the MIT License. -->
<UserControl
x:Class="Peek.FilePreviewer.Controls.UnsupportedFilePreview"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid
Margin="48"
HorizontalAlignment="Stretch"
VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="Icon" Width="Auto" />
<ColumnDefinition x:Name="FileInfo" Width="*" />
</Grid.ColumnDefinitions>
<Image
x:Name="PreviewImage"
Grid.Column="0"
Width="180"
Height="180"
Margin="0,24,24,24"
Source="{x:Bind IconPreview, Mode=OneWay}" />
<StackPanel
Grid.Column="1"
VerticalAlignment="Center"
Spacing="5">
<TextBlock
FontSize="26"
FontWeight="SemiBold"
Text="{x:Bind FileName, Mode=OneWay}"
TextTrimming="CharacterEllipsis" />
<TextBlock Text="{x:Bind FormattedFileType, Mode=OneWay}" />
<TextBlock Text="{x:Bind FormattedFileSize, Mode=OneWay}" />
<TextBlock Text="{x:Bind FormattedDateModified, Mode=OneWay}" />
</StackPanel>
</Grid>
</UserControl>

View File

@@ -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.
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Peek.Common.Helpers;
namespace Peek.FilePreviewer.Controls
{
[INotifyPropertyChanged]
public sealed partial class UnsupportedFilePreview : UserControl
{
[ObservableProperty]
private ImageSource? iconPreview;
[ObservableProperty]
private string? fileName;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FormattedFileType))]
private string? fileType;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FormattedFileSize))]
private string? fileSize;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FormattedDateModified))]
private string? dateModified;
public string FormattedFileType => ReadableStringHelper.FormatResourceString("UnsupportedFile_FileType", FileType);
public string FormattedFileSize => ReadableStringHelper.FormatResourceString("UnsupportedFile_FileSize", FileSize);
public string FormattedDateModified => ReadableStringHelper.FormatResourceString("UnsupportedFile_DateModified", DateModified);
public UnsupportedFilePreview()
{
this.InitializeComponent();
}
}
}

View File

@@ -0,0 +1,25 @@
// 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;
namespace Peek.FilePreviewer.Exceptions
{
public class ImageLoadingException : Exception
{
public ImageLoadingException()
{
}
public ImageLoadingException(string message)
: base(message)
{
}
public ImageLoadingException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

View File

@@ -0,0 +1,53 @@
<!-- Copyright (c) Microsoft Corporation and Contributors. -->
<!-- Licensed under the MIT License. -->
<UserControl
x:Class="Peek.FilePreviewer.FilePreview"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Peek.FilePreviewer.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Peek.FilePreviewer"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:previewers="using:Peek.FilePreviewer.Previewers"
mc:Ignorable="d">
<Grid>
<ProgressRing
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsActive="{x:Bind MatchPreviewState(Previewer.State, previewers:PreviewState.Loading), Mode=OneWay}" />
<Image
x:Name="ImagePreview"
MaxWidth="{x:Bind ImagePreviewer.MaxImageSize.Width, Mode=OneWay}"
MaxHeight="{x:Bind ImagePreviewer.MaxImageSize.Height, Mode=OneWay}"
Source="{x:Bind ImagePreviewer.Preview, Mode=OneWay}"
ToolTipService.ToolTip="{x:Bind ImageInfoTooltip, Mode=OneWay}"
Visibility="{x:Bind IsPreviewVisible(ImagePreviewer, Previewer.State), Mode=OneWay}" />
<controls:BrowserControl
x:Name="BrowserPreview"
x:Load="True"
DOMContentLoaded="BrowserPreview_DOMContentLoaded"
NavigationCompleted="PreviewBrowser_NavigationCompleted"
Source="{x:Bind BrowserPreviewer.Preview, Mode=OneWay}"
IsDevFilePreview="{x:Bind BrowserPreviewer.IsDevFilePreview, Mode=OneWay}"
Visibility="{x:Bind IsPreviewVisible(BrowserPreviewer, Previewer.State), Mode=OneWay, FallbackValue=Collapsed}" />
<controls:UnsupportedFilePreview
x:Name="UnsupportedFilePreview"
DateModified="{x:Bind UnsupportedFilePreviewer.DateModified, Mode=OneWay}"
FileName="{x:Bind UnsupportedFilePreviewer.FileName, Mode=OneWay}"
FileSize="{x:Bind UnsupportedFilePreviewer.FileSize, Mode=OneWay}"
FileType="{x:Bind UnsupportedFilePreviewer.FileType, Mode=OneWay}"
IconPreview="{x:Bind UnsupportedFilePreviewer.IconPreview, Mode=OneWay}"
Visibility="{x:Bind IsPreviewVisible(UnsupportedFilePreviewer, Previewer.State), Mode=OneWay}" />
</Grid>
<UserControl.KeyboardAccelerators>
<KeyboardAccelerator
Key="C"
Invoked="KeyboardAccelerator_CtrlC_Invoked"
Modifiers="Control" />
</UserControl.KeyboardAccelerators>
</UserControl>

View File

@@ -0,0 +1,275 @@
// 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.Globalization;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.Web.WebView2.Core;
using Peek.Common.Extensions;
using Peek.Common.Helpers;
using Peek.Common.Models;
using Peek.FilePreviewer.Models;
using Peek.FilePreviewer.Previewers;
using Peek.FilePreviewer.Previewers.Interfaces;
using Peek.UI.Telemetry.Events;
using Windows.ApplicationModel.Resources;
namespace Peek.FilePreviewer
{
[INotifyPropertyChanged]
public sealed partial class FilePreview : UserControl, IDisposable
{
private readonly PreviewerFactory previewerFactory = new();
public event EventHandler<PreviewSizeChangedArgs>? PreviewSizeChanged;
public static readonly DependencyProperty ItemProperty =
DependencyProperty.Register(
nameof(Item),
typeof(IFileSystemItem),
typeof(FilePreview),
new PropertyMetadata(false, async (d, e) => await ((FilePreview)d).OnItemPropertyChanged()));
public static readonly DependencyProperty ScalingFactorProperty =
DependencyProperty.Register(
nameof(ScalingFactor),
typeof(double),
typeof(FilePreview),
new PropertyMetadata(false, async (d, e) => await ((FilePreview)d).OnScalingFactorPropertyChanged()));
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ImagePreviewer))]
[NotifyPropertyChangedFor(nameof(BrowserPreviewer))]
[NotifyPropertyChangedFor(nameof(UnsupportedFilePreviewer))]
private IPreviewer? previewer;
[ObservableProperty]
private string imageInfoTooltip = ResourceLoader.GetForViewIndependentUse().GetString("PreviewTooltip_Blank");
private CancellationTokenSource _cancellationTokenSource = new();
public FilePreview()
{
InitializeComponent();
}
public void Dispose()
{
_cancellationTokenSource.Dispose();
}
private async void Previewer_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
// Fallback on DefaultPreviewer if we fail to load the correct Preview
if (e.PropertyName == nameof(IPreviewer.State))
{
if (Previewer?.State == PreviewState.Error)
{
// Cancel previous loading task
_cancellationTokenSource.Cancel();
_cancellationTokenSource = new();
Previewer = previewerFactory.CreateDefaultPreviewer(Item);
await UpdatePreviewAsync(_cancellationTokenSource.Token);
}
}
}
public IImagePreviewer? ImagePreviewer => Previewer as IImagePreviewer;
public IBrowserPreviewer? BrowserPreviewer => Previewer as IBrowserPreviewer;
public IUnsupportedFilePreviewer? UnsupportedFilePreviewer => Previewer as IUnsupportedFilePreviewer;
public IFileSystemItem Item
{
get => (IFileSystemItem)GetValue(ItemProperty);
set => SetValue(ItemProperty, value);
}
public double ScalingFactor
{
get => (double)GetValue(ScalingFactorProperty);
set
{
SetValue(ScalingFactorProperty, value);
if (Previewer is IImagePreviewer imagePreviewer)
{
imagePreviewer.ScalingFactor = ScalingFactor;
}
}
}
public bool MatchPreviewState(PreviewState? value, PreviewState stateToMatch)
{
return value == stateToMatch;
}
public Visibility IsPreviewVisible(IPreviewer? previewer, PreviewState? state)
{
var isValidPreview = previewer != null && MatchPreviewState(state, PreviewState.Loaded);
return isValidPreview ? Visibility.Visible : Visibility.Collapsed;
}
private async Task OnItemPropertyChanged()
{
// Cancel previous loading task
_cancellationTokenSource.Cancel();
_cancellationTokenSource = new();
if (Item == null)
{
Previewer = null;
ImagePreview.Visibility = Visibility.Collapsed;
BrowserPreview.Visibility = Visibility.Collapsed;
UnsupportedFilePreview.Visibility = Visibility.Collapsed;
return;
}
Previewer = previewerFactory.Create(Item);
if (Previewer is IImagePreviewer imagePreviewer)
{
imagePreviewer.ScalingFactor = ScalingFactor;
}
await UpdatePreviewAsync(_cancellationTokenSource.Token);
}
private async Task OnScalingFactorPropertyChanged()
{
// Cancel previous loading task
_cancellationTokenSource.Cancel();
_cancellationTokenSource = new();
await UpdatePreviewAsync(_cancellationTokenSource.Token);
}
private async Task UpdatePreviewAsync(CancellationToken cancellationToken)
{
if (Previewer != null)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
var size = await Previewer.GetPreviewSizeAsync(cancellationToken);
PreviewSizeChanged?.Invoke(this, new PreviewSizeChangedArgs(size));
cancellationToken.ThrowIfCancellationRequested();
await Previewer.LoadPreviewAsync(cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
await UpdateImageTooltipAsync(cancellationToken);
}
catch (OperationCanceledException)
{
// TODO: Log task cancelled exception?
}
catch (Exception ex)
{
// Fall back to Default previewer
PowerToysTelemetry.Log.WriteEvent(new ErrorEvent() { HResult = (Common.Models.HResult)ex.HResult, Message = ex.Message, Failure = ErrorEvent.FailureType.PreviewFail });
Logger.LogError("Error in UpdatePreviewAsync, falling back to default previewer: " + ex.Message);
Previewer.State = PreviewState.Error;
}
}
}
partial void OnPreviewerChanging(IPreviewer? value)
{
if (Previewer != null)
{
Previewer.PropertyChanged -= Previewer_PropertyChanged;
}
if (value != null)
{
value.PropertyChanged += Previewer_PropertyChanged;
}
}
private void BrowserPreview_DOMContentLoaded(Microsoft.Web.WebView2.Core.CoreWebView2 sender, Microsoft.Web.WebView2.Core.CoreWebView2DOMContentLoadedEventArgs args)
{
/*
* There is an odd behavior where the WebView2 would not raise the NavigationCompleted event
* for certain HTML files, even though it has already been loaded. Probably related to certain
* extra module that require more time to load. One example is saving and opening google.com locally.
*
* So to address this, we will make the Browser visible and display it as "Loaded" as soon the HTML document
* has been parsed and loaded with the DOMContentLoaded event.
*
* Similar issue: https://github.com/MicrosoftEdge/WebView2Feedback/issues/998
*/
if (BrowserPreviewer != null)
{
BrowserPreviewer.State = PreviewState.Loaded;
}
}
private void PreviewBrowser_NavigationCompleted(WebView2 sender, CoreWebView2NavigationCompletedEventArgs args)
{
/*
* In theory most of navigation should work after DOM is loaded.
* But in case something fails we check NavigationCompleted event
* for failure and switch visibility accordingly.
*
* As an alternative, in the future, the preview Browser control
* could also display error content.
*/
if (!args.IsSuccess)
{
if (BrowserPreviewer != null)
{
BrowserPreviewer.State = PreviewState.Error;
}
}
}
private async void KeyboardAccelerator_CtrlC_Invoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
{
if (Previewer != null)
{
await Previewer.CopyAsync();
}
}
private async Task UpdateImageTooltipAsync(CancellationToken cancellationToken)
{
if (Item == null)
{
return;
}
// Fetch and format available file properties
var sb = new StringBuilder();
string fileNameFormatted = ReadableStringHelper.FormatResourceString("PreviewTooltip_FileName", Item.Name);
sb.Append(fileNameFormatted);
cancellationToken.ThrowIfCancellationRequested();
string fileType = await Task.Run(Item.GetContentTypeAsync);
string fileTypeFormatted = string.IsNullOrEmpty(fileType) ? string.Empty : "\n" + ReadableStringHelper.FormatResourceString("PreviewTooltip_FileType", fileType);
sb.Append(fileTypeFormatted);
string dateModified = Item.DateModified.ToString(CultureInfo.CurrentCulture);
string dateModifiedFormatted = string.IsNullOrEmpty(dateModified) ? string.Empty : "\n" + ReadableStringHelper.FormatResourceString("PreviewTooltip_DateModified", dateModified);
sb.Append(dateModifiedFormatted);
cancellationToken.ThrowIfCancellationRequested();
ulong bytes = await Task.Run(Item.GetSizeInBytes);
string fileSize = ReadableStringHelper.BytesToReadableString(bytes);
string fileSizeFormatted = string.IsNullOrEmpty(fileSize) ? string.Empty : "\n" + ReadableStringHelper.FormatResourceString("PreviewTooltip_FileSize", fileSize);
sb.Append(fileSizeFormatted);
ImageInfoTooltip = sb.ToString();
}
}
}

View File

@@ -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.
using Windows.Foundation;
namespace Peek.FilePreviewer.Models
{
public class PreviewSizeChangedArgs
{
public PreviewSizeChangedArgs(Size? windowSizeRequested)
{
WindowSizeRequested = windowSizeRequested;
}
public Size? WindowSizeRequested { get; init; }
}
}

View File

@@ -0,0 +1,53 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Version.props" />
<PropertyGroup>
<TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
<TargetPlatformMinVersion>10.0.19041.0</TargetPlatformMinVersion>
<RootNamespace>Peek.FilePreviewer</RootNamespace>
<RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Remove="Controls\BrowserControl.xaml" />
<None Remove="FilePreview.xaml" />
<None Remove="UnsupportedFilePreview.xaml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" />
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
<PackageReference Include="System.Drawing.Common" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
<ProjectReference Include="..\..\..\common\FilePreviewCommon\FilePreviewCommon.csproj" />
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
<ProjectReference Include="..\Peek.Common\Peek.Common.csproj" />
<ProjectReference Include="..\WIC\WIC.csproj" />
</ItemGroup>
<ItemGroup>
<Page Update="FilePreview.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="UnsupportedFilePreview.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\BrowserControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Folder Include="Previewers\DrivePreviewer\" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,48 @@
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.UI.Xaml.Media.Imaging;
using Peek.Common;
namespace Peek.FilePreviewer.Previewers.Helpers
{
public static class BitmapHelper
{
public static async Task<BitmapSource> GetBitmapFromHBitmapAsync(IntPtr hbitmap, bool isSupportingTransparency, CancellationToken cancellationToken)
{
try
{
var bitmap = System.Drawing.Image.FromHbitmap(hbitmap);
if (isSupportingTransparency)
{
bitmap.MakeTransparent();
}
var bitmapImage = new BitmapImage();
cancellationToken.ThrowIfCancellationRequested();
using (var stream = new MemoryStream())
{
bitmap.Save(stream, isSupportingTransparency ? ImageFormat.Png : ImageFormat.Bmp);
stream.Position = 0;
cancellationToken.ThrowIfCancellationRequested();
await bitmapImage.SetSourceAsync(stream.AsRandomAccessStream());
}
return bitmapImage;
}
finally
{
// delete HBitmap to avoid memory leaks
NativeMethods.DeleteObject(hbitmap);
}
}
}
}

View File

@@ -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.
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Peek.Common;
using Peek.Common.Models;
namespace Peek.FilePreviewer.Previewers.Helpers
{
public static class IconHelper
{
// 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 async Task<ImageSource?> GetIconAsync(string fileName, CancellationToken cancellationToken)
{
ImageSource? imageSource = null;
IShellItem? nativeShellItem = null;
try
{
Guid shellItem2Guid = new(IShellItem2Guid);
int retCode = NativeMethods.SHCreateItemFromParsingName(fileName, IntPtr.Zero, ref shellItem2Guid, out 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 IntPtr hbitmap);
cancellationToken.ThrowIfCancellationRequested();
if (hr == HResult.Ok)
{
imageSource = await BitmapHelper.GetBitmapFromHBitmapAsync(hbitmap, true, cancellationToken);
}
else
{
var svgImageSource = new SvgImageSource(new Uri("ms-appx:///Assets/DefaultFileIcon.svg"));
imageSource = svgImageSource;
}
}
finally
{
if (nativeShellItem != null)
{
Marshal.ReleaseComObject(nativeShellItem);
}
}
return imageSource;
}
}
}

View File

@@ -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.
using System;
using System.Runtime.InteropServices;
using Peek.Common.Models;
namespace Peek.Common
{
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);
}
}

View File

@@ -0,0 +1,86 @@
// 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.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.UI.Xaml.Media.Imaging;
using Peek.Common;
using Peek.Common.Models;
using Windows.Storage;
namespace Peek.FilePreviewer.Previewers
{
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<NativeSize> ThumbnailFallBackSizes = new List<NativeSize>
{
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;
}
public static async Task<BitmapImage?> GetThumbnailAsync(StorageFile? storageFile, uint size)
{
BitmapImage? bitmapImage = null;
var imageStream = await storageFile?.GetThumbnailAsync(
Windows.Storage.FileProperties.ThumbnailMode.SingleItem,
size,
Windows.Storage.FileProperties.ThumbnailOptions.None);
if (imageStream == null)
{
return bitmapImage;
}
bitmapImage = new BitmapImage();
bitmapImage.SetSource(imageStream);
return bitmapImage;
}
}
}

View File

@@ -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.
using System;
using System.Threading.Tasks;
using WIC;
namespace Peek.FilePreviewer.Previewers
{
public static class WICHelper
{
public static Task<Windows.Foundation.Size> 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);
});
}
}
}

View File

@@ -0,0 +1,329 @@
// 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.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Peek.Common.Extensions;
using Peek.Common.Helpers;
using Peek.Common.Models;
using Peek.FilePreviewer.Exceptions;
using Peek.FilePreviewer.Previewers.Helpers;
using Peek.FilePreviewer.Previewers.Interfaces;
using Windows.Foundation;
namespace Peek.FilePreviewer.Previewers
{
public partial class ImagePreviewer : ObservableObject, IImagePreviewer, IDisposable
{
[ObservableProperty]
private ImageSource? preview;
[ObservableProperty]
private PreviewState state;
[ObservableProperty]
private Size? imageSize;
[ObservableProperty]
private Size maxImageSize;
[ObservableProperty]
private double scalingFactor;
public ImagePreviewer(IFileSystemItem file)
{
Item = file;
Dispatcher = DispatcherQueue.GetForCurrentThread();
}
private IFileSystemItem Item { get; }
private DispatcherQueue Dispatcher { get; }
private Task<bool>? LowQualityThumbnailTask { get; set; }
private Task<bool>? HighQualityThumbnailTask { get; set; }
private Task<bool>? FullQualityImageTask { get; set; }
private bool IsHighQualityThumbnailLoaded => HighQualityThumbnailTask?.Status == TaskStatus.RanToCompletion;
private bool IsFullImageLoaded => FullQualityImageTask?.Status == TaskStatus.RanToCompletion;
public static bool IsFileTypeSupported(string fileExt)
{
return _supportedFileTypes.Contains(fileExt);
}
public void Dispose()
{
GC.SuppressFinalize(this);
}
public async Task<Size?> GetPreviewSizeAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (IsSvg(Item))
{
var size = await Task.Run(Item.GetSvgSize);
if (size != null)
{
ImageSize = size.Value;
}
}
else
{
ImageSize = await Task.Run(Item.GetImageSize);
if (ImageSize == null)
{
ImageSize = await WICHelper.GetImageSize(Item.Path);
}
}
return ImageSize;
}
public async Task LoadPreviewAsync(CancellationToken cancellationToken)
{
State = PreviewState.Loading;
LowQualityThumbnailTask = LoadLowQualityThumbnailAsync(cancellationToken);
HighQualityThumbnailTask = LoadHighQualityThumbnailAsync(cancellationToken);
FullQualityImageTask = LoadFullQualityImageAsync(cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
await Task.WhenAll(LowQualityThumbnailTask, HighQualityThumbnailTask, FullQualityImageTask);
if (Preview == null && HasFailedLoadingPreview())
{
State = PreviewState.Error;
}
}
public async Task CopyAsync()
{
await Dispatcher.RunOnUiThread(async () =>
{
var storageItem = await Item.GetStorageItemAsync();
ClipboardHelper.SaveToClipboard(storageItem);
});
}
partial void OnPreviewChanged(ImageSource? value)
{
if (Preview != null)
{
State = PreviewState.Loaded;
}
}
partial void OnScalingFactorChanged(double value)
{
UpdateMaxImageSize();
}
partial void OnImageSizeChanged(Size? value)
{
UpdateMaxImageSize();
}
private void UpdateMaxImageSize()
{
var imageWidth = ImageSize?.Width ?? 0;
var imageHeight = ImageSize?.Height ?? 0;
if (ScalingFactor != 0)
{
MaxImageSize = new Size(imageWidth / ScalingFactor, imageHeight / ScalingFactor);
}
else
{
MaxImageSize = new Size(imageWidth, imageHeight);
}
}
private Task<bool> LoadLowQualityThumbnailAsync(CancellationToken cancellationToken)
{
return TaskExtension.RunSafe(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
var hr = ThumbnailHelper.GetThumbnail(Path.GetFullPath(Item.Path), out IntPtr hbitmap, ThumbnailHelper.LowQualityThumbnailSize);
if (hr != HResult.Ok)
{
Logger.LogError("Error loading low quality thumbnail - hresult: " + hr);
throw new ImageLoadingException(nameof(hbitmap));
}
cancellationToken.ThrowIfCancellationRequested();
await Dispatcher.RunOnUiThread(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
var thumbnailBitmap = await BitmapHelper.GetBitmapFromHBitmapAsync(hbitmap, IsPng(Item), cancellationToken);
if (!IsFullImageLoaded && !IsHighQualityThumbnailLoaded)
{
Preview = thumbnailBitmap;
}
});
});
}
private Task<bool> LoadHighQualityThumbnailAsync(CancellationToken cancellationToken)
{
return TaskExtension.RunSafe(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
var hr = ThumbnailHelper.GetThumbnail(Path.GetFullPath(Item.Path), out IntPtr hbitmap, ThumbnailHelper.HighQualityThumbnailSize);
if (hr != HResult.Ok)
{
Logger.LogError("Error loading high quality thumbnail - hresult: " + hr);
throw new ImageLoadingException(nameof(hbitmap));
}
cancellationToken.ThrowIfCancellationRequested();
await Dispatcher.RunOnUiThread(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
var thumbnailBitmap = await BitmapHelper.GetBitmapFromHBitmapAsync(hbitmap, IsPng(Item), cancellationToken);
if (!IsFullImageLoaded)
{
Preview = thumbnailBitmap;
}
});
});
}
private Task<bool> LoadFullQualityImageAsync(CancellationToken cancellationToken)
{
return TaskExtension.RunSafe(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
using FileStream stream = File.OpenRead(Item.Path);
await Dispatcher.RunOnUiThread(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
if (IsSvg(Item))
{
var source = new SvgImageSource();
source.RasterizePixelHeight = ImageSize?.Height ?? 0;
source.RasterizePixelWidth = ImageSize?.Width ?? 0;
var loadStatus = await source.SetSourceAsync(stream.AsRandomAccessStream());
if (loadStatus != SvgImageSourceLoadStatus.Success)
{
Logger.LogError("Error loading SVG: " + loadStatus.ToString());
throw new ImageLoadingException(nameof(source));
}
Preview = source;
}
else
{
var bitmap = new BitmapImage();
await bitmap.SetSourceAsync(stream.AsRandomAccessStream());
Preview = bitmap;
}
});
});
}
private bool HasFailedLoadingPreview()
{
var hasFailedLoadingLowQualityThumbnail = !(LowQualityThumbnailTask?.Result ?? true);
var hasFailedLoadingHighQualityThumbnail = !(HighQualityThumbnailTask?.Result ?? true);
var hasFailedLoadingFullQualityImage = !(FullQualityImageTask?.Result ?? true);
return hasFailedLoadingLowQualityThumbnail && hasFailedLoadingHighQualityThumbnail && hasFailedLoadingFullQualityImage;
}
private bool IsPng(IFileSystemItem item)
{
return item.Extension == ".png";
}
private bool IsSvg(IFileSystemItem item)
{
return item.Extension == ".svg";
}
private static readonly HashSet<string> _supportedFileTypes = new HashSet<string>
{
// Image types
".bmp",
".gif",
".jpg",
".jfif",
".jfi",
".jif",
".jpeg",
".jpe",
".png",
".tif", // very slow for large files: no thumbnail?
".tiff", // NEED TO TEST
".dib", // NEED TO TEST
".heic",
".heif",
".hif", // NEED TO TEST
".avif", // NEED TO TEST
".jxr",
".wdp",
".ico", // NEED TO TEST
".thumb", // NEED TO TEST
// Raw types
".arw",
".cr2",
".crw",
".erf",
".kdc", // NEED TO TEST
".mrw",
".nef",
".nrw",
".orf",
".pef",
".raf",
".raw",
".rw2",
".rwl",
".sr2",
".srw",
".srf",
".dcs", // NEED TO TEST
".dcr",
".drf", // NEED TO TEST
".k25",
".3fr",
".ari", // NEED TO TEST
".bay", // NEED TO TEST
".cap", // NEED TO TEST
".iiq",
".eip", // NEED TO TEST
".fff",
".mef",
// ".mdc", // Crashes in GetFullBitmapFromPathAsync
".mos",
".R3D",
".rwz", // NEED TO TEST
".x3f",
".ori",
".cr3",
".svg",
};
}
}

View File

@@ -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.
using System;
namespace Peek.FilePreviewer.Previewers
{
public interface IBrowserPreviewer : IPreviewer
{
public Uri? Preview { get; }
public bool IsDevFilePreview { get; }
}
}

View File

@@ -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.
using Microsoft.UI.Xaml.Media;
using Windows.Foundation;
namespace Peek.FilePreviewer.Previewers.Interfaces
{
public interface IImagePreviewer : IPreviewer
{
public ImageSource? Preview { get; }
public double ScalingFactor { get; set; }
public Size MaxImageSize { get; set; }
}
}

View File

@@ -0,0 +1,33 @@
// 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.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using Windows.Foundation;
namespace Peek.FilePreviewer.Previewers
{
public interface IPreviewer : INotifyPropertyChanged
{
PreviewState State { get; set; }
public static bool IsFileTypeSupported(string fileExt) => throw new NotImplementedException();
public Task<Size?> GetPreviewSizeAsync(CancellationToken cancellationToken);
Task LoadPreviewAsync(CancellationToken cancellationToken);
Task CopyAsync();
}
public enum PreviewState
{
Uninitialized,
Loading,
Loaded,
Error,
}
}

View File

@@ -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 Microsoft.UI.Xaml.Media;
namespace Peek.FilePreviewer.Previewers
{
public interface IUnsupportedFilePreviewer : IPreviewer
{
public ImageSource? IconPreview { get; }
public string? FileName { get; }
public string? FileType { get; }
public string? FileSize { get; }
public string? DateModified { get; }
}
}

View File

@@ -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 Microsoft.PowerToys.Telemetry;
using Peek.Common.Models;
using Peek.UI.Telemetry.Events;
namespace Peek.FilePreviewer.Previewers
{
public class PreviewerFactory
{
public IPreviewer Create(IFileSystemItem file)
{
if (ImagePreviewer.IsFileTypeSupported(file.Extension))
{
return new ImagePreviewer(file);
}
else if (WebBrowserPreviewer.IsFileTypeSupported(file.Extension))
{
return new WebBrowserPreviewer(file);
}
// Other previewer types check their supported file types here
return CreateDefaultPreviewer(file);
}
public IPreviewer CreateDefaultPreviewer(IFileSystemItem file)
{
PowerToysTelemetry.Log.WriteEvent(new ErrorEvent() { Failure = ErrorEvent.FailureType.FileNotSupported });
return new UnsupportedFilePreviewer(file);
}
}
}

View File

@@ -0,0 +1,148 @@
// 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.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Media;
using Peek.Common.Extensions;
using Peek.Common.Helpers;
using Peek.Common.Models;
using Peek.FilePreviewer.Previewers.Helpers;
using Windows.Foundation;
namespace Peek.FilePreviewer.Previewers
{
public partial class UnsupportedFilePreviewer : ObservableObject, IUnsupportedFilePreviewer, IDisposable
{
[ObservableProperty]
private ImageSource? iconPreview;
[ObservableProperty]
private string? fileName;
[ObservableProperty]
private string? fileType;
[ObservableProperty]
private string? fileSize;
[ObservableProperty]
private string? dateModified;
[ObservableProperty]
private PreviewState state;
public UnsupportedFilePreviewer(IFileSystemItem file)
{
Item = file;
FileName = file.Name;
DateModified = file.DateModified.ToString(CultureInfo.CurrentCulture);
Dispatcher = DispatcherQueue.GetForCurrentThread();
}
public bool IsPreviewLoaded => iconPreview != null;
private IFileSystemItem Item { get; }
private DispatcherQueue Dispatcher { get; }
private Task<bool>? IconPreviewTask { get; set; }
private Task<bool>? DisplayInfoTask { get; set; }
public void Dispose()
{
GC.SuppressFinalize(this);
}
public Task<Size?> GetPreviewSizeAsync(CancellationToken cancellationToken)
{
Size? size = new Size(680, 500);
return Task.FromResult(size);
}
public async Task LoadPreviewAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
State = PreviewState.Loading;
IconPreviewTask = LoadIconPreviewAsync(cancellationToken);
DisplayInfoTask = LoadDisplayInfoAsync(cancellationToken);
await Task.WhenAll(IconPreviewTask, DisplayInfoTask);
if (HasFailedLoadingPreview())
{
State = PreviewState.Error;
}
}
public async Task CopyAsync()
{
await Dispatcher.RunOnUiThread(async () =>
{
var storageItem = await Item.GetStorageItemAsync();
ClipboardHelper.SaveToClipboard(storageItem);
});
}
public Task<bool> LoadIconPreviewAsync(CancellationToken cancellationToken)
{
return TaskExtension.RunSafe(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
await Dispatcher.RunOnUiThread(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
var iconBitmap = await IconHelper.GetIconAsync(Path.GetFullPath(Item.Path), cancellationToken);
IconPreview = iconBitmap;
});
});
}
public Task<bool> LoadDisplayInfoAsync(CancellationToken cancellationToken)
{
return TaskExtension.RunSafe(async () =>
{
// File Properties
cancellationToken.ThrowIfCancellationRequested();
var bytes = await Task.Run(Item.GetSizeInBytes);
cancellationToken.ThrowIfCancellationRequested();
var type = await Task.Run(Item.GetContentTypeAsync);
await Dispatcher.RunOnUiThread(() =>
{
FileSize = ReadableStringHelper.BytesToReadableString(bytes);
FileType = type;
return Task.CompletedTask;
});
});
}
partial void OnIconPreviewChanged(ImageSource? value)
{
if (IconPreview != null)
{
State = PreviewState.Loaded;
}
}
private bool HasFailedLoadingPreview()
{
var hasFailedLoadingIconPreview = !(IconPreviewTask?.Result ?? true);
var hasFailedLoadingDisplayInfo = !(DisplayInfoTask?.Result ?? true);
return hasFailedLoadingIconPreview && hasFailedLoadingDisplayInfo;
}
}
}

View File

@@ -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.IO;
using Common.UI;
namespace Peek.FilePreviewer.Previewers
{
public class MarkdownHelper
{
/// <summary>
/// Prepares temp html for the previewing
/// </summary>
public static string PreviewTempFile(string fileText, string filePath, string tempFolder)
{
string theme = ThemeManager.GetWindowsBaseColor().ToLowerInvariant();
string markdownHTML = Microsoft.PowerToys.FilePreviewCommon.MarkdownHelper.MarkdownHtml(fileText, theme, filePath, ImageBlockedCallback);
string filename = tempFolder + "\\" + Guid.NewGuid().ToString() + ".html";
File.WriteAllText(filename, markdownHTML);
return filename;
}
private static void ImageBlockedCallback()
{
}
}
}

View File

@@ -0,0 +1,66 @@
// 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.Text.Json;
using Common.UI;
namespace Peek.FilePreviewer.Previewers
{
public class MonacoHelper
{
public static HashSet<string> GetExtensions()
{
HashSet<string> set = new HashSet<string>();
try
{
JsonDocument languageListDocument = Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.GetLanguages();
JsonElement languageList = languageListDocument.RootElement.GetProperty("list");
foreach (JsonElement e in languageList.EnumerateArray())
{
for (int j = 0; j < e.GetProperty("extensions").GetArrayLength(); j++)
{
set.Add(e.GetProperty("extensions")[j].ToString());
}
}
}
catch (Exception)
{
}
return set;
}
/// <summary>
/// Prepares temp html for the previewing
/// </summary>
public static string PreviewTempFile(string fileText, string extension, string tempFolder)
{
// TODO: check if file is too big, add MaxFileSize to settings
return InitializeIndexFileAndSelectedFile(fileText, extension, tempFolder);
}
private static string InitializeIndexFileAndSelectedFile(string fileContent, string extension, string tempFolder)
{
string vsCodeLangSet = Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.GetLanguage(extension);
string base64FileCode = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(fileContent));
string theme = ThemeManager.GetWindowsBaseColor().ToLowerInvariant();
// prepping index html to load in
string html = Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.ReadIndexHtml();
html = html.Replace("[[PT_LANG]]", vsCodeLangSet, StringComparison.InvariantCulture);
html = html.Replace("[[PT_WRAP]]", "1", StringComparison.InvariantCulture); // TODO: add to settings
html = html.Replace("[[PT_THEME]]", theme, StringComparison.InvariantCulture);
html = html.Replace("[[PT_CODE]]", base64FileCode, StringComparison.InvariantCulture);
html = html.Replace("[[PT_URL]]", Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.VirtualHostName, StringComparison.InvariantCulture);
string filename = tempFolder + "\\" + Guid.NewGuid().ToString() + ".html";
File.WriteAllText(filename, html);
return filename;
}
}
}

View File

@@ -0,0 +1,22 @@
// 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.IO;
using System.Text;
using System.Threading.Tasks;
namespace Peek.FilePreviewer.Previewers
{
public static class ReadHelper
{
public static async Task<string> Read(string path)
{
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read);
using var sr = new StreamReader(fs, Encoding.UTF8);
string content = await sr.ReadToEndAsync();
return content;
}
}
}

View File

@@ -0,0 +1,131 @@
// 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.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Dispatching;
using Peek.Common.Constants;
using Peek.Common.Extensions;
using Peek.Common.Helpers;
using Peek.Common.Models;
using Windows.Foundation;
namespace Peek.FilePreviewer.Previewers
{
public partial class WebBrowserPreviewer : ObservableObject, IBrowserPreviewer, IDisposable
{
private static readonly HashSet<string> _supportedFileTypes = new HashSet<string>
{
// Web
".html",
".htm",
// Document
".pdf",
// Markdown
".md",
};
private static readonly HashSet<string> _supportedMonacoFileTypes = MonacoHelper.GetExtensions();
[ObservableProperty]
private Uri? preview;
[ObservableProperty]
private PreviewState state;
[ObservableProperty]
private bool isDevFilePreview;
public WebBrowserPreviewer(IFileSystemItem file)
{
File = file;
Dispatcher = DispatcherQueue.GetForCurrentThread();
}
public void Dispose()
{
GC.SuppressFinalize(this);
}
private IFileSystemItem File { get; }
public bool IsPreviewLoaded => preview != null;
private DispatcherQueue Dispatcher { get; }
private Task<bool>? DisplayInfoTask { get; set; }
public Task<Size?> GetPreviewSizeAsync(CancellationToken cancellationToken)
{
Size? size = null;
return Task.FromResult(size);
}
public async Task LoadPreviewAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
State = PreviewState.Loading;
await LoadDisplayInfoAsync(cancellationToken);
if (HasFailedLoadingPreview())
{
State = PreviewState.Error;
}
}
public Task<bool> LoadDisplayInfoAsync(CancellationToken cancellationToken)
{
return TaskExtension.RunSafe(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
await Dispatcher.RunOnUiThread(async () =>
{
bool isHtml = File.Extension == ".html";
bool isMarkdown = File.Extension == ".md";
IsDevFilePreview = _supportedMonacoFileTypes.Contains(File.Extension);
if (IsDevFilePreview && !isHtml && !isMarkdown)
{
var raw = await ReadHelper.Read(File.Path.ToString());
Preview = new Uri(MonacoHelper.PreviewTempFile(raw, File.Extension, TempFolderPath.Path));
}
else if (isMarkdown)
{
var raw = await ReadHelper.Read(File.Path.ToString());
Preview = new Uri(MarkdownHelper.PreviewTempFile(raw, File.Path, TempFolderPath.Path));
}
else
{
Preview = new Uri(File.Path);
}
});
});
}
public async Task CopyAsync()
{
await Dispatcher.RunOnUiThread(async () =>
{
var storageItem = await File.GetStorageItemAsync();
ClipboardHelper.SaveToClipboard(storageItem);
});
}
public static bool IsFileTypeSupported(string fileExt)
{
return _supportedFileTypes.Contains(fileExt) || _supportedMonacoFileTypes.Contains(fileExt);
}
private bool HasFailedLoadingPreview()
{
return !(DisplayInfoTask?.Result ?? true);
}
}
}

View File

@@ -0,0 +1,18 @@
<!-- Copyright (c) Microsoft Corporation and Contributors. -->
<!-- Licensed under the MIT License. -->
<Application
x:Class="Peek.UI.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Peek.UI">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<!-- Other merged dictionaries here -->
</ResourceDictionary.MergedDictionaries>
<!-- Other app resources here -->
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -0,0 +1,106 @@
// 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 ManagedCommon;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Xaml;
using Peek.FilePreviewer;
using Peek.UI.Telemetry.Events;
using Peek.UI.Views;
using WinUIEx;
namespace Peek.UI
{
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public partial class App : Application
{
public static int PowerToysPID { get; set; }
public IHost Host
{
get;
}
private Window? Window { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="App"/> 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().
/// </summary>
public App()
{
InitializeComponent();
Host = Microsoft.Extensions.Hosting.Host.
CreateDefaultBuilder().
UseContentRoot(AppContext.BaseDirectory).
ConfigureServices((context, services) =>
{
// Core Services
services.AddTransient<NeighboringItemsQuery>();
// Views and ViewModels
services.AddTransient<TitleBar>();
services.AddTransient<FilePreview>();
services.AddTransient<MainWindowViewModel>();
}).
Build();
UnhandledException += App_UnhandledException;
}
public static T GetService<T>()
where T : class
{
if ((App.Current as App)!.Host.Services.GetService(typeof(T)) is not T service)
{
throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices within App.xaml.cs.");
}
return service;
}
/// <summary>
/// Invoked when the application is launched.
/// </summary>
/// <param name="args">Details about the launch request and process.</param>
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredPeekEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
{
Logger.LogWarning("Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.");
Environment.Exit(0); // Current.Exit won't work until there's a window opened.
return;
}
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 void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
PowerToysTelemetry.Log.WriteEvent(new ErrorEvent() { HResult = (Common.Models.HResult)e.Exception.HResult, Failure = ErrorEvent.FailureType.AppCrash });
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 745 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,5 @@
<svg width="auto" height="auto" viewBox="0 0 357 357" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M72.5156 323.531H284.484C287.561 323.531 290.062 321.029 290.062 317.953V100.406H239.859C230.632 100.406 223.125 92.8995 223.125 83.6719V33.4688H72.5156C69.4393 33.4688 66.9375 35.9705 66.9375 39.0469V317.953C66.9375 321.029 69.4393 323.531 72.5156 323.531Z" fill="white"/>
<path d="M284.964 89.25L234.281 38.5685V83.6719C234.281 86.7482 236.783 89.25 239.859 89.25H284.964Z" fill="white"/>
<path opacity="0.64" fill-rule="evenodd" clip-rule="evenodd" d="M301.219 94.8281C301.215 94.0033 301.026 93.1899 300.667 92.4476C299.925 89.5635 298.423 86.9315 296.317 84.8265L238.705 27.2156C235.574 24.0657 231.312 22.3 226.871 22.3124H72.5156C63.288 22.3124 55.7812 29.8192 55.7812 39.0468V317.953C55.7812 327.181 63.288 334.687 72.5156 334.687H284.484C293.712 334.687 301.219 327.181 301.219 317.953V96.6605C301.219 96.2629 301.191 95.8704 301.163 95.4781L301.159 95.4249C301.165 95.3614 301.174 95.2985 301.182 95.2357C301.201 95.1015 301.219 94.9677 301.219 94.8281ZM284.964 89.25H239.859C236.783 89.25 234.281 86.7482 234.281 83.6718V38.5685L284.964 89.25ZM72.5156 323.531H284.484C287.561 323.531 290.062 321.029 290.062 317.953V100.406H239.859C230.632 100.406 223.125 92.8995 223.125 83.6718V33.4687H72.5156C69.4393 33.4687 66.9375 35.9705 66.9375 39.0468V317.953C66.9375 321.029 69.4393 323.531 72.5156 323.531Z" fill="#605E5C"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -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.
using System.Text;
using Peek.UI.Native;
using Windows.Win32;
using Windows.Win32.Foundation;
namespace Peek.UI.Extensions
{
public static class HWNDExtensions
{
internal static HWND GetActiveTab(this HWND windowHandle)
{
var activeTab = windowHandle.FindChildWindow("ShellTabWindowClass");
if (activeTab == HWND.Null)
{
activeTab = windowHandle.FindChildWindow("TabWindowClass");
}
return activeTab;
}
internal static bool IsDesktopWindow(this HWND windowHandle)
{
StringBuilder strClassName = new StringBuilder(256);
var result = NativeMethods.GetClassName(windowHandle, strClassName, 256);
if (result == 0)
{
return false;
}
var className = strClassName.ToString();
if (className != "Progman" && className != "WorkerW")
{
return false;
}
return windowHandle.FindChildWindow("SHELLDLL_DefView") != HWND.Null;
}
internal static HWND FindChildWindow(this HWND windowHandle, string className)
{
return PInvoke.FindWindowEx(windowHandle, HWND.Null, className, null);
}
}
}

View File

@@ -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.IO;
using Peek.Common.Helpers;
using Peek.Common.Models;
namespace Peek.UI.Extensions
{
public static class IShellItemExtensions
{
public static IFileSystemItem ToIFileSystemItem(this IShellItem shellItem)
{
string path = string.Empty;
try
{
path = shellItem.GetDisplayName(Windows.Win32.UI.Shell.SIGDN.SIGDN_FILESYSPATH);
}
catch (Exception ex)
{
// TODO: Handle cases that do not have a file system path like Recycle Bin.
Logger.LogError("Getting path failed. " + ex.Message);
}
return File.Exists(path) ? new FileItem(path) : new FolderItem(path);
}
}
}

View File

@@ -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.
using Windows.Foundation;
namespace Peek.UI.Extensions
{
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);
}
}
}

View File

@@ -0,0 +1,57 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.UI.Xaml;
using Windows.Foundation;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Gdi;
using WinUIEx;
namespace Peek.UI.Extensions
{
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 = default(MONITORINFO);
info.cbSize = 40;
PInvoke.GetMonitorInfo(hwndDesktop, ref info);
double monitorWidth = info.rcMonitor.left + info.rcMonitor.right;
double monitorHeight = info.rcMonitor.bottom + info.rcMonitor.top;
return new Size(monitorWidth, monitorHeight);
}
public static double GetMonitorScale(this Window window)
{
var hwnd = new HWND(window.GetWindowHandle());
var dpi = PInvoke.GetDpiForWindow(new HWND(hwnd));
double scalingFactor = dpi / 96d;
return scalingFactor;
}
public static void BringToForeground(this Window window)
{
var foregroundWindowHandle = PInvoke.GetForegroundWindow();
uint targetProcessId = 0;
uint windowThreadProcessId = 0;
unsafe
{
windowThreadProcessId = PInvoke.GetWindowThreadProcessId(foregroundWindowHandle, &targetProcessId);
}
var windowHandle = window.GetWindowHandle();
var currentThreadId = PInvoke.GetCurrentThreadId();
PInvoke.AttachThreadInput(windowThreadProcessId, currentThreadId, true);
PInvoke.BringWindowToTop(new HWND(windowHandle));
PInvoke.ShowWindow(new HWND(windowHandle), Windows.Win32.UI.WindowsAndMessaging.SHOW_WINDOW_CMD.SW_SHOW);
PInvoke.AttachThreadInput(windowThreadProcessId, currentThreadId, false);
}
}
}

View File

@@ -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.Runtime.InteropServices;
using System.Text;
using Peek.Common.Helpers;
using Peek.Common.Models;
using Peek.UI.Native;
namespace Peek.UI.Helpers
{
public static class DefaultAppHelper
{
public static string TryGetDefaultAppName(string extension)
{
string appName = string.Empty;
// Get the length of the app name
uint length = 0;
HResult ret = NativeMethods.AssocQueryString(NativeMethods.AssocF.Verify, NativeMethods.AssocStr.FriendlyAppName, extension, null, null, ref length);
if (ret != HResult.False)
{
Logger.LogError($"Error when getting accessString for {extension} file: {Marshal.GetExceptionForHR((int)ret)!.Message}");
return appName;
}
// Get the app name
StringBuilder sb = new((int)length);
ret = NativeMethods.AssocQueryString(NativeMethods.AssocF.Verify, NativeMethods.AssocStr.FriendlyAppName, extension, null, sb, ref length);
if (ret != HResult.Ok)
{
Logger.LogError($"Error when getting accessString for {extension} file: {Marshal.GetExceptionForHR((int)ret)!.Message}" );
return appName;
}
appName = sb.ToString();
return appName;
}
}
}

View File

@@ -0,0 +1,96 @@
// 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.Common.Models;
using Peek.UI.Extensions;
using SHDocVw;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Shell;
using IServiceProvider = Peek.Common.Models.IServiceProvider;
namespace Peek.UI.Helpers
{
public static class FileExplorerHelper
{
internal static IShellItemArray? GetSelectedItems(HWND foregroundWindowHandle)
{
return GetItemsInternal(foregroundWindowHandle, onlySelectedFiles: true);
}
internal static IShellItemArray? GetItems(HWND foregroundWindowHandle)
{
return GetItemsInternal(foregroundWindowHandle, onlySelectedFiles: false);
}
private static IShellItemArray? GetItemsInternal(HWND foregroundWindowHandle, bool onlySelectedFiles)
{
if (foregroundWindowHandle.IsDesktopWindow())
{
return GetItemsFromDesktop(foregroundWindowHandle, onlySelectedFiles);
}
else
{
return GetItemsFromFileExplorer(foregroundWindowHandle, onlySelectedFiles);
}
}
private static IShellItemArray? GetItemsFromDesktop(HWND foregroundWindowHandle, bool onlySelectedFiles)
{
const int SWC_DESKTOP = 8;
const int SWFO_NEEDDISPATCH = 1;
var shell = new Shell32.Shell();
ShellWindows shellWindows = shell.Windows();
object? oNull1 = null;
object? oNull2 = null;
var serviceProvider = (IServiceProvider)shellWindows.FindWindowSW(ref oNull1, ref oNull2, SWC_DESKTOP, out int pHWND, SWFO_NEEDDISPATCH);
var shellBrowser = (IShellBrowser)serviceProvider.QueryService(PInvoke.SID_STopLevelBrowser, typeof(IShellBrowser).GUID);
IShellItemArray? shellItemArray = GetShellItemArray(shellBrowser, onlySelectedFiles);
return shellItemArray;
}
private static IShellItemArray? GetItemsFromFileExplorer(HWND foregroundWindowHandle, bool onlySelectedFiles)
{
IShellItemArray? shellItemArray = null;
var activeTab = foregroundWindowHandle.GetActiveTab();
var shell = new Shell32.Shell();
ShellWindows shellWindows = shell.Windows();
foreach (IWebBrowserApp webBrowserApp in shell.Windows())
{
var shellFolderView = (Shell32.IShellFolderViewDual2)webBrowserApp.Document;
var folderTitle = shellFolderView.Folder.Title;
if (webBrowserApp.HWND == foregroundWindowHandle)
{
var serviceProvider = (IServiceProvider)webBrowserApp;
var shellBrowser = (IShellBrowser)serviceProvider.QueryService(PInvoke.SID_STopLevelBrowser, typeof(IShellBrowser).GUID);
shellBrowser.GetWindow(out IntPtr shellBrowserHandle);
if (activeTab == shellBrowserHandle)
{
shellItemArray = GetShellItemArray(shellBrowser, onlySelectedFiles);
return shellItemArray;
}
}
}
return shellItemArray;
}
private static IShellItemArray? GetShellItemArray(IShellBrowser shellBrowser, bool onlySelectedFiles)
{
var shellView = (IFolderView)shellBrowser.QueryActiveShellView();
var selectionFlag = onlySelectedFiles ? (uint)_SVGIO.SVGIO_SELECTION : (uint)_SVGIO.SVGIO_ALLVIEW;
shellView.Items(selectionFlag, typeof(IShellItemArray).GUID, out var items);
return items as IShellItemArray;
}
}
}

View File

@@ -0,0 +1,43 @@
<!-- Copyright (c) Microsoft Corporation and Contributors. -->
<!-- Licensed under the MIT License. -->
<winuiex:WindowEx
x:Class="Peek.UI.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:fp="using:Peek.FilePreviewer"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="using:Peek.UI.Views"
xmlns:winuiex="using:WinUIEx"
mc:Ignorable="d">
<winuiex:WindowEx.Backdrop>
<winuiex:MicaSystemBackdrop />
</winuiex:WindowEx.Backdrop>
<Grid KeyboardAcceleratorPlacementMode="Hidden">
<Grid.KeyboardAccelerators>
<KeyboardAccelerator Key="Left" Invoked="LeftNavigationInvoked" />
<KeyboardAccelerator Key="Right" Invoked="RightNavigationInvoked" />
</Grid.KeyboardAccelerators>
<Grid.RowDefinitions>
<RowDefinition Height="48" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<views:TitleBar
x:Name="TitleBarControl"
Grid.Row="0"
FileIndex="{x:Bind ViewModel.CurrentIndex, Mode=OneWay}"
IsMultiSelection="{x:Bind ViewModel.NeighboringItemsQuery.IsMultipleFilesActivation, Mode=OneWay}"
Item="{x:Bind ViewModel.CurrentItem, Mode=OneWay}"
NumberOfFiles="{x:Bind ViewModel.Items.Count, Mode=OneWay}" />
<fp:FilePreview
Grid.Row="1"
Item="{x:Bind ViewModel.CurrentItem, Mode=OneWay}"
PreviewSizeChanged="FilePreviewer_PreviewSizeChanged"
ScalingFactor="{x:Bind ViewModel.ScalingFactor, Mode=OneWay}" />
</Grid>
</winuiex:WindowEx>

View File

@@ -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.
using interop;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml.Input;
using Peek.Common.Constants;
using Peek.FilePreviewer.Models;
using Peek.UI.Extensions;
using Peek.UI.Helpers;
using Peek.UI.Native;
using Peek.UI.Telemetry.Events;
using Windows.Foundation;
using WinUIEx;
namespace Peek.UI
{
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainWindow : WindowEx
{
public MainWindowViewModel ViewModel { get; }
public MainWindow()
{
InitializeComponent();
ViewModel = App.GetService<MainWindowViewModel>();
NativeEventWaiter.WaitForEventLoop(Constants.ShowPeekEvent(), OnPeekHotkey);
TitleBarControl.SetTitleBarToWindow(this);
AppWindow.Closing += AppWindow_Closing;
}
/// <summary>
/// Handle Peek hotkey, by toggling the window visibility and querying files when necessary.
/// </summary>
private void OnPeekHotkey()
{
if (AppWindow.IsVisible)
{
if (IsNewSingleSelectedItem())
{
Initialize();
}
else
{
Uninitialize();
}
}
else
{
Initialize();
}
}
private void LeftNavigationInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
{
ViewModel.AttemptLeftNavigation();
}
private void RightNavigationInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
{
ViewModel.AttemptRightNavigation();
}
private void Initialize()
{
var bootTime = new System.Diagnostics.Stopwatch();
bootTime.Start();
ViewModel.Initialize();
ViewModel.ScalingFactor = this.GetMonitorScale();
bootTime.Stop();
PowerToysTelemetry.Log.WriteEvent(new OpenedEvent() { FileExtension = ViewModel.CurrentItem?.Extension ?? string.Empty, HotKeyToVisibleTimeMs = bootTime.ElapsedMilliseconds });
}
private void Uninitialize()
{
this.Restore();
this.Hide();
ViewModel.Uninitialize();
ViewModel.ScalingFactor = 1;
}
/// <summary>
/// Handle FilePreviewerSizeChanged event to adjust window size and position accordingly.
/// </summary>
/// <param name="sender">object</param>
/// <param name="e">PreviewSizeChangedArgs</param>
private void FilePreviewer_PreviewSizeChanged(object sender, PreviewSizeChangedArgs e)
{
var monitorSize = this.GetMonitorSize();
// If no size is requested, try to fit to the monitor size.
Size requestedSize = e.WindowSizeRequested ?? monitorSize;
double titleBarHeight = TitleBarControl.ActualHeight;
double maxContentWidth = monitorSize.Width * WindowConstants.MaxWindowToMonitorRatio;
double maxContentHeight = (monitorSize.Height - titleBarHeight) * WindowConstants.MaxWindowToMonitorRatio;
Size maxContentSize = new(maxContentWidth, maxContentHeight);
double minContentWidth = WindowConstants.MinWindowWidth;
double minContentHeight = WindowConstants.MinWindowHeight - titleBarHeight;
Size minContentSize = new(minContentWidth, minContentHeight);
Size 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
double monitorScale = this.GetMonitorScale();
double scaledWindowWidth = adjustedContentSize.Width / monitorScale;
double scaledWindowHeight = adjustedContentSize.Height / monitorScale;
double desiredScaledHeight = scaledWindowHeight + titleBarHeight + WindowConstants.WindowWidthContentPadding;
double desiredScaledWidth = scaledWindowWidth + WindowConstants.WindowHeightContentPadding;
if (!TitleBarControl.Pinned)
{
this.CenterOnScreen(desiredScaledWidth, desiredScaledHeight); // re-center if not pinned
}
this.Show();
this.BringToForeground();
}
/// <summary>
/// Handle AppWindow closing to prevent app termination on close.
/// </summary>
/// <param name="sender">AppWindow</param>
/// <param name="args">AppWindowClosingEventArgs</param>
private void AppWindow_Closing(AppWindow sender, AppWindowClosingEventArgs args)
{
args.Cancel = true;
PowerToysTelemetry.Log.WriteEvent(new ClosedEvent());
Uninitialize();
}
private bool IsNewSingleSelectedItem()
{
var foregroundWindowHandle = Windows.Win32.PInvoke.GetForegroundWindow();
var selectedItems = FileExplorerHelper.GetSelectedItems(foregroundWindowHandle);
var selectedItemsCount = selectedItems?.GetCount() ?? 0;
if (selectedItems == null || selectedItemsCount == 0 || selectedItemsCount > 1)
{
return false;
}
var fileExplorerSelectedItemPath = selectedItems.GetItemAt(0).ToIFileSystemItem().Path;
var currentItemPath = ViewModel.CurrentItem?.Path;
if (fileExplorerSelectedItemPath == null || currentItemPath == null || fileExplorerSelectedItemPath == currentItemPath)
{
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,99 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml;
using Peek.Common.Helpers;
using Peek.Common.Models;
using Peek.UI.Models;
namespace Peek.UI
{
public partial class MainWindowViewModel : ObservableObject
{
private const int NavigationThrottleDelayMs = 100;
[ObservableProperty]
private int _currentIndex;
[ObservableProperty]
private IFileSystemItem? _currentItem;
[ObservableProperty]
private NeighboringItems? _items;
[ObservableProperty]
private double _scalingFactor = 1.0;
public NeighboringItemsQuery NeighboringItemsQuery { get; }
private DispatcherTimer NavigationThrottleTimer { get; set; } = new();
public MainWindowViewModel(NeighboringItemsQuery query)
{
NeighboringItemsQuery = query;
NavigationThrottleTimer.Tick += NavigationThrottleTimer_Tick;
NavigationThrottleTimer.Interval = TimeSpan.FromMilliseconds(NavigationThrottleDelayMs);
}
public void Initialize()
{
Items = NeighboringItemsQuery.GetNeighboringItems();
CurrentIndex = 0;
if (Items != null && Items.Count > 0)
{
CurrentItem = Items[0];
}
}
public void Uninitialize()
{
CurrentIndex = 0;
CurrentItem = null;
Items = null;
}
public void AttemptLeftNavigation()
{
if (NavigationThrottleTimer.IsEnabled)
{
return;
}
NavigationThrottleTimer.Start();
var itemCount = Items?.Count ?? 1;
CurrentIndex = MathHelper.Modulo(CurrentIndex - 1, itemCount);
CurrentItem = Items?.ElementAtOrDefault(CurrentIndex);
}
public void AttemptRightNavigation()
{
if (NavigationThrottleTimer.IsEnabled)
{
return;
}
NavigationThrottleTimer.Start();
var itemCount = Items?.Count ?? 1;
CurrentIndex = MathHelper.Modulo(CurrentIndex + 1, itemCount);
CurrentItem = Items?.ElementAtOrDefault(CurrentIndex);
}
private void NavigationThrottleTimer_Tick(object? sender, object e)
{
if (sender == null)
{
return;
}
((DispatcherTimer)sender).Stop();
}
}
}

View File

@@ -0,0 +1,39 @@
// 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;
using System.Collections.Generic;
using Peek.Common.Models;
using Peek.UI.Extensions;
namespace Peek.UI.Models
{
public class NeighboringItems : IReadOnlyList<IFileSystemItem>
{
public IFileSystemItem this[int index] => Items[index] = Items[index] ?? ShellItemArray.GetItemAt(index).ToIFileSystemItem();
public int Count { get; }
private IFileSystemItem[] Items { get; }
private IShellItemArray ShellItemArray { get; }
public NeighboringItems(IShellItemArray shellItemArray)
{
ShellItemArray = shellItemArray;
Count = ShellItemArray.GetCount();
Items = new IFileSystemItem[Count];
}
public IEnumerator<IFileSystemItem> GetEnumerator()
{
return new NeighboringItemsEnumerator(this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

View File

@@ -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.
using System;
using System.Collections;
using System.Collections.Generic;
using Peek.Common.Models;
namespace Peek.UI.Models
{
public class NeighboringItemsEnumerator : IEnumerator<IFileSystemItem>
{
public IFileSystemItem Current => Items[CurrentIndex];
object IEnumerator.Current => Current;
private int CurrentIndex { get; set; }
private NeighboringItems Items { get; }
public NeighboringItemsEnumerator(NeighboringItems items)
{
CurrentIndex = -1;
Items = items;
}
public void Dispose()
{
GC.SuppressFinalize(this);
}
public bool MoveNext()
{
if (CurrentIndex >= Items.Count)
{
return false;
}
CurrentIndex++;
return true;
}
public void Reset()
{
CurrentIndex = -1;
}
}
}

View File

@@ -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 System.Text;
using Peek.Common.Models;
namespace Peek.UI.Native
{
public static class NativeMethods
{
[Flags]
public enum AssocF
{
None = 0,
Init_NoRemapCLSID = 0x1,
Init_ByExeName = 0x2,
Open_ByExeName = 0x3,
Init_DefaultToStar = 0x4,
Init_DefaultToFolder = 0x8,
NoUserSettings = 0x10,
NoTruncate = 0x20,
Verify = 0x40,
RemapRunDll = 0x80,
NoFixUps = 0x100,
IgnoreBaseClass = 0x200,
}
public enum AssocStr
{
Command = 1,
Executable,
FriendlyDocName,
FriendlyAppName,
NoOpen,
ShellNewValue,
DDECommand,
DDEIfExec,
DDEApplication,
DDETopic,
}
[DllImport("Shlwapi.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern HResult AssocQueryString(AssocF flags, AssocStr str, string pszAssoc, string? pszExtra, [Out] StringBuilder? pszOut, [In][Out] ref uint pcchOut);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
internal static extern int GetWindowText(Windows.Win32.Foundation.HWND hWnd, StringBuilder lpString, int nMaxCount);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
internal static extern int GetClassName(IntPtr hWnd, StringBuilder buf, int nMaxCount);
}
}

View File

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

View File

@@ -0,0 +1,14 @@
MonitorFromWindow
GetMonitorInfo
GetDpiForWindow
GetForegroundWindow
GetWindowThreadProcessId
GetCurrentThreadId
AttachThreadInput
BringWindowToTop
ShowWindow
GetWindowTextLength
FindWindowEx
SID_STopLevelBrowser
GetClassName
_SVGIO

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<Identity
Name="62c2f436-8802-4c26-a73d-b3b8613016fd"
Publisher="CN=sachaple"
Version="1.0.0.0" />
<Properties>
<DisplayName>Peek.UI</DisplayName>
<PublisherDisplayName>sachaple</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate"/>
</Resources>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="Peek.UI"
Description="Peek.UI"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View File

@@ -0,0 +1,118 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Version.props" />
<PropertyGroup>
<AssemblyName>PowerToys.Peek.UI</AssemblyName>
<AssemblyTitle>PowerToys.Peek.UI</AssemblyTitle>
<AssemblyDescription>PowerToys Peek UI</AssemblyDescription>
<RootNamespace>Peek.UI</RootNamespace>
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
<TargetPlatformMinVersion>10.0.19041.0</TargetPlatformMinVersion>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\modules\Peek\</OutputPath>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Platforms>x86;x64;ARM64</Platforms>
<RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
<WindowsPackageType>None</WindowsPackageType>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<SupportedOSPlatformVersion>10.0.19041.0</SupportedOSPlatformVersion>
<Nullable>Enable</Nullable>
<ApplicationIcon>Assets\Icon.ico</ApplicationIcon>
<SelfContained>true</SelfContained>
</PropertyGroup>
<!-- SelfContained=true requires RuntimeIdentifier to be set -->
<PropertyGroup Condition="'$(Platform)'=='x64'">
<RuntimeIdentifier>win10-x64</RuntimeIdentifier>
</PropertyGroup>
<PropertyGroup Condition="'$(Platform)'=='ARM64'">
<RuntimeIdentifier>win10-arm64</RuntimeIdentifier>
</PropertyGroup>
<!-- See https://learn.microsoft.com/windows/apps/develop/platform/csharp-winrt/net-projection-from-cppwinrt-component for more info -->
<PropertyGroup>
<CsWinRTIncludes>PowerToys.GPOWrapper</CsWinRTIncludes>
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
</PropertyGroup>
<ItemGroup>
<Content Include="Assets\Icon.ico" />
</ItemGroup>
<ItemGroup>
<None Remove="Assets\AppList.scale-100.png" />
<None Remove="Assets\AppList.scale-125.png" />
<None Remove="Assets\AppList.scale-150.png" />
<None Remove="Assets\AppList.scale-200.png" />
<None Remove="Assets\AppList.scale-400.png" />
<None Remove="Views\TitleBar.xaml" />
<None Remove="Views\UnsupportedFile.xaml" />
</ItemGroup>
<ItemGroup>
<COMReference Include="Shell32">
<VersionMinor>0</VersionMinor>
<VersionMajor>1</VersionMajor>
<Guid>50a7e9b0-70ef-11d1-b75a-00a0c90564fe</Guid>
<Lcid>0</Lcid>
<WrapperTool>tlbimp</WrapperTool>
<Isolated>false</Isolated>
<EmbedInteropTypes>true</EmbedInteropTypes>
</COMReference>
<COMReference Include="SHDocVw">
<VersionMinor>1</VersionMinor>
<VersionMajor>1</VersionMajor>
<Guid>eab22ac0-30c1-11cf-a7eb-0000c05bae0b</Guid>
<Lcid>0</Lcid>
<WrapperTool>tlbimp</WrapperTool>
<Isolated>false</Isolated>
<EmbedInteropTypes>true</EmbedInteropTypes>
</COMReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="Microsoft.Windows.CsWinRT" />
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" />
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
<PackageReference Include="WinUIEx" />
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<!--
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
Tools extension to be activated for this project even if the Windows App SDK Nuget
package has not yet been restored.
-->
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<ProjectCapability Include="Msix" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\Peek.Common\Peek.Common.csproj" />
<ProjectReference Include="..\Peek.FilePreviewer\Peek.FilePreviewer.csproj" />
</ItemGroup>
<ItemGroup>
<Resource Include="Assets\Icon.ico" />
</ItemGroup>
<ItemGroup>
<Page Update="Views\UnsupportedFile.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Views\TitleBar.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,43 @@
// 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.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel;
using Peek.Common.Models;
using Peek.UI.Extensions;
using Peek.UI.Helpers;
using Peek.UI.Models;
namespace Peek.UI
{
public partial class NeighboringItemsQuery : ObservableObject
{
[ObservableProperty]
private bool isMultipleFilesActivation;
public NeighboringItems? GetNeighboringItems()
{
var foregroundWindowHandle = Windows.Win32.PInvoke.GetForegroundWindow();
var selectedItemsShellArray = FileExplorerHelper.GetSelectedItems(foregroundWindowHandle);
var selectedItemsCount = selectedItemsShellArray?.GetCount() ?? 0;
if (selectedItemsShellArray == null || selectedItemsCount < 1)
{
return null;
}
bool hasMoreThanOneItem = selectedItemsCount > 1;
IsMultipleFilesActivation = hasMoreThanOneItem;
var neighboringItemsShellArray = hasMoreThanOneItem ? selectedItemsShellArray : FileExplorerHelper.GetItems(foregroundWindowHandle);
if (neighboringItemsShellArray == null)
{
return null;
}
return new NeighboringItems(neighboringItemsShellArray);
}
}
}

View File

@@ -0,0 +1,216 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="AppTitle.Title" xml:space="preserve">
<value>Peek</value>
<comment>Name of application.</comment>
</data>
<data name="AppTitle_FileCounts_Text" xml:space="preserve">
<value>({0}/{1})</value>
<comment>Text for the file count in the title bar. 0: the index of the current file. 1: the total number of files selected.</comment>
</data>
<data name="LaunchAppButton_OpenWith_Text" xml:space="preserve">
<value>Open with</value>
<comment>Text for button to launch the application picker.</comment>
</data>
<data name="LaunchAppButton_OpenWith_ToolTip" xml:space="preserve">
<value>Open with (Enter)</value>
<comment>Tooltip for button to launch the application picker.</comment>
</data>
<data name="LaunchAppButton_OpenWithApp_Text" xml:space="preserve">
<value>Open with {0}</value>
<comment>Text for button to launch default application. 0: name of the default application.</comment>
</data>
<data name="LaunchAppButton_OpenWithApp_ToolTip" xml:space="preserve">
<value>Open with {0} (Enter)</value>
<comment>Tooltip for button to launch default application. 0: name of the default application.</comment>
</data>
<data name="UnsupportedFile_FileType" xml:space="preserve">
<value>File Type: {0}</value>
<comment>File Type label for the unsupported files view. {0} is the type.</comment>
</data>
<data name="UnsupportedFile_FileSize" xml:space="preserve">
<value>Size: {0}</value>
<comment>File Size label for the unsupported files view. {0} is the size.</comment>
</data>
<data name="UnsupportedFile_DateModified" xml:space="preserve">
<value>Date Modified: {0}</value>
<comment>Date Modified label for the unsupported files view. {0} is the date.</comment>
</data>
<data name="ReadableString_ByteAbbreviationFormat" xml:space="preserve">
<value>{0} bytes</value>
<comment>Abbreviation for the size unit byte.</comment>
</data>
<data name="ReadableString_KiloByteAbbreviationFormat" xml:space="preserve">
<value>{0} KB</value>
<comment>Abbreviation for the size unit kilobyte.</comment>
</data>
<data name="ReadableString_MegaByteAbbreviationFormat" xml:space="preserve">
<value>{0} MB</value>
<comment>Abbreviation for the size unit megabyte.</comment>
</data>
<data name="ReadableString_GigaByteAbbreviationFormat" xml:space="preserve">
<value>{0} GB</value>
<comment>Abbreviation for the size unit gigabyte.</comment>
</data>
<data name="ReadableString_TeraByteAbbreviationFormat" xml:space="preserve">
<value>{0} TB</value>
<comment>Abbreviation for the size unit terabyte.</comment>
</data>
<data name="ReadableString_PetaByteAbbreviationFormat" xml:space="preserve">
<value>{0} PB</value>
<comment>Abbreviation for the size unit petabyte.</comment>
</data>
<data name="ReadableString_ExaByteAbbreviationFormat" xml:space="preserve">
<value>{0} EB</value>
<comment>Abbreviation for the size unit exabyte.</comment>
</data>
<data name="PreviewTooltip_FileName" xml:space="preserve">
<value>Filename: {0}</value>
<comment>Filename for the tooltip of preview. {0} is the name.</comment>
</data>
<data name="PreviewTooltip_FileType" xml:space="preserve">
<value>Item Type: {0}</value>
<comment>Item Type for the tooltip of preview. {0} is the type.</comment>
</data>
<data name="PreviewTooltip_DateModified" xml:space="preserve">
<value>Date Modified: {0}</value>
<comment>Date Modified label for the tooltip of preview. {0} is the date.</comment>
</data>
<data name="PreviewTooltip_Dimensions" xml:space="preserve">
<value>Dimensions: {0} x {1}</value>
<comment>Dimensions label for the tooltip of preview. {0} is the width, {1} is the height.</comment>
</data>
<data name="PreviewTooltip_FileSize" xml:space="preserve">
<value>Size: {0}</value>
<comment>File Size label for the tooltip of preview. {0} is the size.</comment>
</data>
<data name="PreviewTooltip_Blank" xml:space="preserve">
<value>File preview</value>
<comment>Tooltip of preview when there's no file info available.</comment>
</data>
<data name="PinButton_Tooltip" xml:space="preserve">
<value>Pin the window to the current location</value>
<comment>Tooltip for button to pin the Peek window.</comment>
</data>
<data name="UnpinButton_ToolTip" xml:space="preserve">
<value>Unpin the window</value>
<comment>Tooltip for button to unpin the Peek window.</comment>
</data>
</root>

View File

@@ -0,0 +1,149 @@
<!-- Copyright (c) Microsoft Corporation and Contributors. -->
<!-- Licensed under the MIT License. -->
<UserControl
x:Class="Peek.UI.Views.TitleBar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Peek.UI.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid x:Name="TitleBarRootContainer" Height="48">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="SystemLeftPaddingColumn" Width="0" />
<ColumnDefinition x:Name="DraggableColumn" Width="*" />
<ColumnDefinition x:Name="LaunchAppButtonColumn" Width="Auto" />
<ColumnDefinition x:Name="AppRightPaddingColumn" Width="65" />
<ColumnDefinition x:Name="PinButtonColumn" Width="40" />
<ColumnDefinition x:Name="SystemRightPaddingColumn" Width="0" />
</Grid.ColumnDefinitions>
<Grid
x:Name="AppIconAndFileTitleContainer"
Grid.Column="1"
Margin="8,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
ColumnSpacing="4">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="AppIconColumn" Width="32" />
<ColumnDefinition x:Name="FileTitleColumn" Width="*" />
</Grid.ColumnDefinitions>
<Image
x:Name="PeekLogo"
x:Uid="PeekLogo"
Grid.Column="0"
Width="24"
Height="24"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Source="../Assets/AppList.scale-400.png"
Stretch="UniformToFill" />
<Grid
x:Name="FileCountAndNameContainer"
Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Center"
ColumnSpacing="4">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="FileCountColumn" Width="auto" />
<ColumnDefinition x:Name="FileNameColumn" Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
x:Name="AppTitle_FileCount"
x:Uid="AppTitle_FileCount"
Grid.Column="0"
FontWeight="Bold"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind FileCountText, Mode=OneWay}"
Visibility="{x:Bind IsMultiSelection, Mode=OneWay}" />
<TextBlock
x:Name="AppTitle_FileName"
x:Uid="AppTitle_FileName"
Grid.Column="1"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Item.Name, Mode=OneWay}"
TextWrapping="NoWrap" />
</Grid>
</Grid>
<Button
x:Name="LaunchAppButton"
x:Uid="LaunchAppButton"
Grid.Column="2"
VerticalAlignment="Center"
Command="{x:Bind LaunchDefaultAppButtonAsyncCommand, Mode=OneWay}"
ToolTipService.ToolTip="{x:Bind OpenWithAppToolTip, Mode=OneWay}"
Visibility="{x:Bind IsLaunchDefaultAppButtonVisible(DefaultAppName), Mode=OneWay}">
<Button.Content>
<StackPanel Orientation="Horizontal" Spacing="4">
<FontIcon
x:Name="LaunchAppButton_Icon"
x:Uid="LaunchAppButton_Icon"
FontSize="{StaticResource CaptionTextBlockFontSize}"
Glyph="&#xE8E5;" />
<TextBlock
x:Name="LaunchAppButton_Text"
x:Uid="LaunchAppButton_Text"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind OpenWithAppText, Mode=OneWay}" />
</StackPanel>
</Button.Content>
<Button.KeyboardAccelerators>
<KeyboardAccelerator Key="Enter" />
</Button.KeyboardAccelerators>
</Button>
<Button
x:Name="PinButton"
x:Uid="PinButton"
Grid.Column="4"
VerticalAlignment="Center"
Command="{x:Bind PinCommand, Mode=OneWay}"
ToolTipService.ToolTip="{x:Bind PinToolTip(Pinned), Mode=OneWay}">
<Button.Content>
<FontIcon
x:Name="PinButton_Icon"
x:Uid="PinButton_Icon"
FontSize="{StaticResource CaptionTextBlockFontSize}"
Glyph="{x:Bind PinGlyph(Pinned), Mode=OneWay}" />
</Button.Content>
</Button>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="AdaptiveWidth">
<VisualState x:Name="MaximumLayout">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="560" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="LaunchAppButton_Text.Visibility" Value="Visible" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="MediumLayout">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="340" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="LaunchAppButton_Text.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="MinimumLayout">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="LaunchAppButton_Text.Visibility" Value="Collapsed" />
<Setter Target="LaunchAppButton.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</UserControl>

View File

@@ -0,0 +1,272 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Globalization;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ManagedCommon;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Peek.Common.Models;
using Peek.UI.Extensions;
using Peek.UI.Helpers;
using Peek.UI.Telemetry.Events;
using Windows.ApplicationModel.Resources;
using Windows.Graphics;
using Windows.Storage;
using Windows.System;
using WinUIEx;
namespace Peek.UI.Views
{
[INotifyPropertyChanged]
public sealed partial class TitleBar : UserControl
{
public static readonly DependencyProperty ItemProperty =
DependencyProperty.Register(
nameof(Item),
typeof(IFileSystemItem),
typeof(TitleBar),
new PropertyMetadata(null, (d, e) => ((TitleBar)d).OnFilePropertyChanged()));
public static readonly DependencyProperty FileIndexProperty =
DependencyProperty.Register(
nameof(FileIndex),
typeof(int),
typeof(TitleBar),
new PropertyMetadata(-1, (d, e) => ((TitleBar)d).OnFileIndexPropertyChanged()));
public static readonly DependencyProperty IsMultiSelectionProperty =
DependencyProperty.Register(
nameof(IsMultiSelection),
typeof(bool),
typeof(TitleBar),
new PropertyMetadata(false));
public static readonly DependencyProperty NumberOfFilesProperty =
DependencyProperty.Register(
nameof(NumberOfFiles),
typeof(int),
typeof(TitleBar),
new PropertyMetadata(null, null));
[ObservableProperty]
private string openWithAppText = ResourceLoader.GetForViewIndependentUse().GetString("LaunchAppButton_OpenWith_Text");
[ObservableProperty]
private string openWithAppToolTip = ResourceLoader.GetForViewIndependentUse().GetString("LaunchAppButton_OpenWith_ToolTip");
[ObservableProperty]
private string? fileCountText;
[ObservableProperty]
private string defaultAppName = string.Empty;
[ObservableProperty]
private bool pinned = false;
public TitleBar()
{
InitializeComponent();
TitleBarRootContainer.SizeChanged += TitleBarRootContainer_SizeChanged;
}
public IFileSystemItem Item
{
get => (IFileSystemItem)GetValue(ItemProperty);
set => SetValue(ItemProperty, value);
}
public int FileIndex
{
get => (int)GetValue(FileIndexProperty);
set => SetValue(FileIndexProperty, value);
}
public bool IsMultiSelection
{
get => (bool)GetValue(IsMultiSelectionProperty);
set => SetValue(IsMultiSelectionProperty, value);
}
public int NumberOfFiles
{
get => (int)GetValue(NumberOfFilesProperty);
set => SetValue(NumberOfFilesProperty, value);
}
private Window? MainWindow { get; set; }
public void SetTitleBarToWindow(MainWindow mainWindow)
{
MainWindow = mainWindow;
if (AppWindowTitleBar.IsCustomizationSupported())
{
UpdateTitleBarCustomization(mainWindow);
}
else
{
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
ThemeHelpers.SetImmersiveDarkMode(hWnd, ThemeHelpers.GetAppTheme() == AppTheme.Dark);
Visibility = Visibility.Collapsed;
// Set window icon
WindowId windowId = Win32Interop.GetWindowIdFromWindow(hWnd);
AppWindow appWindow = AppWindow.GetFromWindowId(windowId);
appWindow.SetIcon("Assets/Icon.ico");
}
}
public Visibility IsLaunchDefaultAppButtonVisible(string appName)
{
return string.IsNullOrEmpty(appName) ? Visibility.Collapsed : Visibility.Visible;
}
[RelayCommand]
private async void LaunchDefaultAppButtonAsync()
{
if (Item is not FileItem fileItem)
{
return;
}
StorageFile? storageFile = await fileItem.GetStorageFileAsync();
LauncherOptions options = new();
PowerToysTelemetry.Log.WriteEvent(new OpenWithEvent() { App = DefaultAppName ?? string.Empty });
if (string.IsNullOrEmpty(DefaultAppName))
{
// If there's no default app found, open the App picker
options.DisplayApplicationPicker = true;
}
else
{
// Try to launch the default app for current file format
bool result = await Launcher.LaunchFileAsync(storageFile, options);
if (!result)
{
// If we couldn't successfully open the default app, open the App picker as a fallback
options.DisplayApplicationPicker = true;
await Launcher.LaunchFileAsync(storageFile, options);
}
}
}
public string PinGlyph(bool pinned)
{
return pinned ? "\xE840" : "\xE718";
}
public string PinToolTip(bool pinned)
{
return pinned ? ResourceLoader.GetForViewIndependentUse().GetString("UnpinButton_ToolTip") : ResourceLoader.GetForViewIndependentUse().GetString("PinButton_ToolTip");
}
[RelayCommand]
private void Pin()
{
Pinned = !Pinned;
}
private void TitleBarRootContainer_SizeChanged(object sender, SizeChangedEventArgs e)
{
UpdateDragRegion();
}
private void UpdateDragRegion()
{
if (MainWindow == null)
{
return;
}
var appWindow = MainWindow.GetAppWindow();
if (AppWindowTitleBar.IsCustomizationSupported() && appWindow != null && appWindow.TitleBar.ExtendsContentIntoTitleBar)
{
var scale = MainWindow.GetMonitorScale();
SystemRightPaddingColumn.Width = new GridLength(appWindow.TitleBar.RightInset / scale);
SystemLeftPaddingColumn.Width = new GridLength(appWindow.TitleBar.LeftInset / scale);
var dragRectsList = new List<RectInt32>();
RectInt32 dragRectangleLeft;
dragRectangleLeft.X = (int)(SystemLeftPaddingColumn.ActualWidth * scale);
dragRectangleLeft.Y = 0;
dragRectangleLeft.Height = (int)(TitleBarRootContainer.ActualHeight * scale);
dragRectangleLeft.Width = (int)(DraggableColumn.ActualWidth * scale);
RectInt32 dragRectangleRight;
dragRectangleRight.X = (int)((SystemLeftPaddingColumn.ActualWidth + DraggableColumn.ActualWidth + LaunchAppButtonColumn.ActualWidth) * scale);
dragRectangleRight.Y = 0;
dragRectangleRight.Height = (int)(TitleBarRootContainer.ActualHeight * scale);
dragRectangleRight.Width = (int)(AppRightPaddingColumn.ActualWidth * scale);
dragRectsList.Add(dragRectangleLeft);
dragRectsList.Add(dragRectangleRight);
appWindow.TitleBar.SetDragRectangles(dragRectsList.ToArray());
}
}
private void UpdateTitleBarCustomization(MainWindow mainWindow)
{
if (AppWindowTitleBar.IsCustomizationSupported())
{
AppWindow appWindow = mainWindow.GetAppWindow();
appWindow.TitleBar.ExtendsContentIntoTitleBar = true;
appWindow.TitleBar.ButtonBackgroundColor = Colors.Transparent;
appWindow.TitleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
mainWindow.SetTitleBar(this);
}
}
private void OnFilePropertyChanged()
{
if (Item == null)
{
return;
}
UpdateFileCountText();
UpdateDefaultAppToLaunch();
}
private void OnFileIndexPropertyChanged()
{
UpdateFileCountText();
}
private void UpdateFileCountText()
{
// Update file count
if (NumberOfFiles > 1)
{
string fileCountTextFormat = ResourceLoader.GetForViewIndependentUse().GetString("AppTitle_FileCounts_Text");
FileCountText = string.Format(CultureInfo.InvariantCulture, fileCountTextFormat, FileIndex + 1, NumberOfFiles);
}
}
private void UpdateDefaultAppToLaunch()
{
// Update the name of default app to launch
DefaultAppName = DefaultAppHelper.TryGetDefaultAppName(Item.Extension);
string openWithAppTextFormat = ResourceLoader.GetForViewIndependentUse().GetString("LaunchAppButton_OpenWithApp_Text");
OpenWithAppText = string.Format(CultureInfo.InvariantCulture, openWithAppTextFormat, DefaultAppName);
string openWithAppToolTipFormat = ResourceLoader.GetForViewIndependentUse().GetString("LaunchAppButton_OpenWithApp_ToolTip");
OpenWithAppToolTip = string.Format(CultureInfo.InvariantCulture, openWithAppToolTipFormat, DefaultAppName);
}
}
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="Peek.UI.app"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!--The ID below informs the system that this application is compatible with OS features first introduced in Windows 8.
For more info see https://docs.microsoft.com/windows/win32/sysinfo/targeting-your-application-at-windows-8-1
It is also necessary to support features in unpackaged applications, for example the custom title bar implementation.-->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<!-- The combination of below two tags have the following effect:
1) Per-Monitor for >= Windows 10 Anniversary Update
2) System < Windows 10 Anniversary Update
-->
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
</windowsSettings>
</application>
</assembly>

View File

@@ -0,0 +1,7 @@
namespace WIC
{
internal static class CLSID
{
public const string WICImagingFactory = "cacaf262-9370-4615-a13b-9f5539da4c0a";
}
}

View File

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

View File

@@ -0,0 +1,43 @@
using System;
using System.Runtime.InteropServices;
namespace WIC
{
internal struct CoTaskMemPtr : IDisposable
{
public static CoTaskMemPtr From<T>(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);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More