Skip to content

Commit

Permalink
[Peek] Add basic file querying and navigation (#22589)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
estebanm123 and Esteban Margaron authored Dec 7, 2022
1 parent e1cb01d commit d4e618c
Show file tree
Hide file tree
Showing 7 changed files with 286 additions and 43 deletions.
2 changes: 2 additions & 0 deletions src/modules/peek/Peek.FilePreviewer/FilePreview.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ public File File

private async Task OnFilePropertyChanged()
{
// TODO: track and cancel existing async preview tasks
// https://github.com/microsoft/PowerToys/issues/22480
if (File == null)
{
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace Peek.FilePreviewer.Previewers
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing.Imaging;
using System.IO;
using System.Threading;
Expand Down Expand Up @@ -83,10 +84,17 @@ private Task LoadLowQualityThumbnailAsync()
if (!IsFullImageLoaded && !IsHighQualityThumbnailLoaded)
{
// TODO: Handle thumbnail errors
ThumbnailHelper.GetThumbnail(Path.GetFullPath(File.Path), out IntPtr hbitmap, ThumbnailHelper.LowQualityThumbnailSize);
var thumbnailBitmap = await GetBitmapFromHBitmapAsync(hbitmap);
Preview = thumbnailBitmap;
var hr = ThumbnailHelper.GetThumbnail(Path.GetFullPath(File.Path), out IntPtr hbitmap, ThumbnailHelper.LowQualityThumbnailSize);
if (hr == Common.Models.HResult.Ok)
{
var thumbnailBitmap = await GetBitmapFromHBitmapAsync(hbitmap);
Preview = thumbnailBitmap;
}
else
{
// TODO: handle thumbnail errors
Debug.WriteLine("Error loading thumbnail - hresult: " + hr);
}
}
thumbnailTCS.SetResult();
Expand All @@ -108,11 +116,18 @@ private Task LoadHighQualityThumbnailAsync()
if (!IsFullImageLoaded)
{
// TODO: Handle thumbnail errors
ThumbnailHelper.GetThumbnail(Path.GetFullPath(File.Path), out IntPtr hbitmap, ThumbnailHelper.HighQualityThumbnailSize);
var thumbnailBitmap = await GetBitmapFromHBitmapAsync(hbitmap);
IsHighQualityThumbnailLoaded = true;
Preview = thumbnailBitmap;
var hr = ThumbnailHelper.GetThumbnail(Path.GetFullPath(File.Path), out IntPtr hbitmap, ThumbnailHelper.HighQualityThumbnailSize);
if (hr == Common.Models.HResult.Ok)
{
var thumbnailBitmap = await GetBitmapFromHBitmapAsync(hbitmap);
IsHighQualityThumbnailLoaded = true;
Preview = thumbnailBitmap;
}
else
{
// TODO: handle thumbnail errors
Debug.WriteLine("Error loading thumbnail - hresult: " + hr);
}
}
thumbnailTCS.SetResult();
Expand Down
170 changes: 170 additions & 0 deletions src/modules/peek/Peek.UI/FolderItemsQuery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace Peek.UI
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using Peek.Common.Models;
using Peek.UI.Helpers;

public partial class FolderItemsQuery : ObservableObject
{
private const int UninitializedItemIndex = -1;

public void Clear()
{
CurrentFile = null;

if (InitializeFilesTask != null && InitializeFilesTask.Status == TaskStatus.Running)
{
Debug.WriteLine("Detected existing initializeFilesTask running. Cancelling it..");
CancellationTokenSource.Cancel();
}

InitializeFilesTask = null;

lock (_mutateQueryDataLock)
{
Files = new List<File>();
_currentItemIndex = UninitializedItemIndex;
}
}

public void UpdateCurrentItemIndex(int desiredIndex)
{
if (Files.Count <= 1 || _currentItemIndex == UninitializedItemIndex ||
(InitializeFilesTask != null && InitializeFilesTask.Status == TaskStatus.Running))
{
return;
}

// Current index wraps around when reaching min/max folder item indices
desiredIndex %= Files.Count;
_currentItemIndex = desiredIndex < 0 ? Files.Count + desiredIndex : desiredIndex;

if (_currentItemIndex < 0 || _currentItemIndex >= Files.Count)
{
Debug.Assert(false, "Out of bounds folder item index detected.");
_currentItemIndex = 0;
}

CurrentFile = Files[_currentItemIndex];
}

public void Start()
{
var folderView = FileExplorerHelper.GetCurrentFolderView();
if (folderView == null)
{
return;
}

Shell32.FolderItems selectedItems = folderView.SelectedItems();
if (selectedItems == null || selectedItems.Count == 0)
{
return;
}

// Prioritize setting CurrentFile, which notifies UI
var firstSelectedItem = selectedItems.Item(0);
CurrentFile = new File(firstSelectedItem.Path);

var items = selectedItems.Count > 1 ? selectedItems : folderView.Folder?.Items();
if (items == null)
{
return;
}

try
{
if (InitializeFilesTask != null && InitializeFilesTask.Status == TaskStatus.Running)
{
Debug.WriteLine("Detected unexpected existing initializeFilesTask running. Cancelling it..");
CancellationTokenSource.Cancel();
}

CancellationTokenSource = new CancellationTokenSource();
InitializeFilesTask = new Task(() => InitializeFiles(items, firstSelectedItem, CancellationTokenSource.Token));

// Execute file initialization/querying on background thread
InitializeFilesTask.Start();
}
catch (Exception e)
{
Debug.WriteLine("Exception trying to run initializeFilesTask:\n" + e.ToString());
}
}

// Finds index of firstSelectedItem either amongst folder items, initializing our internal File list
// since storing Shell32.FolderItems as a field isn't reliable.
// Can take a few seconds for folders with 1000s of items; ensure it runs on a background thread.
//
// TODO optimization:
// Handle case where selected items count > 1 separately. Although it'll still be slow for 1000s of items selected,
// we can leverage faster APIs like Windows.Storage when 1 item is selected, and navigation is scoped to
// the entire folder. We can then avoid iterating through all items here, and maintain a dynamic window of
// loaded items around the current item index.
private void InitializeFiles(
Shell32.FolderItems items,
Shell32.FolderItem firstSelectedItem,
CancellationToken cancellationToken)
{
var tempFiles = new List<File>(items.Count);
var tempCurIndex = UninitializedItemIndex;

for (int i = 0; i < items.Count; i++)
{
cancellationToken.ThrowIfCancellationRequested();

var item = items.Item(i);
if (item == null)
{
continue;
}

if (item.Name == firstSelectedItem.Name)
{
tempCurIndex = i;
}

tempFiles.Add(new File(item.Path));
}

if (tempCurIndex == UninitializedItemIndex)
{
Debug.WriteLine("File query initialization: selectedItem index not found. Navigation remains disabled.");
return;
}

cancellationToken.ThrowIfCancellationRequested();

lock (_mutateQueryDataLock)
{
cancellationToken.ThrowIfCancellationRequested();
Files = tempFiles;
_currentItemIndex = tempCurIndex;
}
}

private readonly object _mutateQueryDataLock = new ();

[ObservableProperty]
private File? _currentFile;

private List<File> Files { get; set; } = new ();

private int _currentItemIndex = UninitializedItemIndex;

public int CurrentItemIndex => _currentItemIndex;

private CancellationTokenSource CancellationTokenSource { get; set; } = new CancellationTokenSource();

private Task? InitializeFilesTask { get; set; } = null;
}
}
17 changes: 6 additions & 11 deletions src/modules/peek/Peek.UI/Helpers/FileExplorerHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,30 @@
// See the LICENSE file in the project root for more information.

using System.Collections.Generic;
using System.Diagnostics;
using Peek.Common.Models;
using Peek.UI.Native;

namespace Peek.UI.Helpers
{
public static class FileExplorerHelper
{
public static List<File> GetSelectedFileExplorerFiles()
public static Shell32.IShellFolderViewDual2? GetCurrentFolderView()
{
var foregroundWindowHandle = NativeMethods.GetForegroundWindow();

var selectedItems = new List<File>();
var shell = new Shell32.Shell();
foreach (SHDocVw.InternetExplorer window in shell.Windows())
{
// TODO: figure out which window is the active explorer tab
// https://github.com/microsoft/PowerToys/issues/22507
if (window.HWND == (int)foregroundWindowHandle)
{
Shell32.FolderItems items = ((Shell32.IShellFolderViewDual2)window.Document).SelectedItems();
if (items != null && items.Count > 0)
{
foreach (Shell32.FolderItem item in items)
{
selectedItems.Add(new File(item.Path));
}
}
return (Shell32.IShellFolderViewDual2)window.Document;
}
}

return selectedItems;
return null;
}
}
}
9 changes: 7 additions & 2 deletions src/modules/peek/Peek.UI/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@
<winuiex:MicaSystemBackdrop />
</winuiex:WindowEx.Backdrop>

<Grid>
<Grid KeyboardAcceleratorPlacementMode="Hidden">
<Grid.KeyboardAccelerators>
<KeyboardAccelerator Key="Left" Invoked="LeftNavigationInvoked" />
<KeyboardAccelerator Key="Right" Invoked="RightNavigationInvoked" />
</Grid.KeyboardAccelerators>

<Grid.RowDefinitions>
<RowDefinition Height="32" />
<RowDefinition Height="*" />
Expand All @@ -25,7 +30,7 @@

<fp:FilePreview
Grid.Row="1"
File="{x:Bind ViewModel.CurrentFile, Mode=OneWay}"
File="{x:Bind ViewModel.FolderItemsQuery.CurrentFile, Mode=OneWay}"
PreviewSizeChanged="FilePreviewer_PreviewSizeChanged" />
</Grid>
</winuiex:WindowEx>
45 changes: 29 additions & 16 deletions src/modules/peek/Peek.UI/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@

namespace Peek.UI
{
using System.Collections.Generic;
using System.Linq;
using interop;
using Microsoft.UI.Windowing;
using Peek.Common.Models;
using Microsoft.UI.Xaml.Input;
using Peek.FilePreviewer.Models;
using Peek.UI.Extensions;
using Peek.UI.Helpers;
using Peek.UI.Native;
using Windows.Foundation;
using Windows.System;
using Windows.UI.Core;
using WinUIEx;

/// <summary>
Expand Down Expand Up @@ -49,23 +48,37 @@ private void OnPeekHotkey()
{
if (AppWindow.IsVisible)
{
this.Hide();
ViewModel.Files = new List<File>();
ViewModel.CurrentFile = null;
Uninitialize();
}
else
{
var fileExplorerSelectedFiles = FileExplorerHelper.GetSelectedFileExplorerFiles();
if (fileExplorerSelectedFiles.Count == 0)
{
return;
}

ViewModel.Files = fileExplorerSelectedFiles;
ViewModel.CurrentFile = fileExplorerSelectedFiles.First();
Initialize();
}
}

private void LeftNavigationInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
{
ViewModel.AttemptLeftNavigation();
}

private void RightNavigationInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
{
ViewModel.AttemptRightNavigation();
}

private void Initialize()
{
ViewModel.FolderItemsQuery.Start();
}

private void Uninitialize()
{
this.Hide();

// TODO: move into general ViewModel method when needed
ViewModel.FolderItemsQuery.Clear();
}

/// <summary>
/// Handle FilePreviewerSizeChanged event to adjust window size and position accordingly.
/// </summary>
Expand Down Expand Up @@ -101,7 +114,7 @@ private void FilePreviewer_PreviewSizeChanged(object sender, PreviewSizeChangedA
private void AppWindow_Closing(AppWindow sender, AppWindowClosingEventArgs args)
{
args.Cancel = true;
this.Hide();
Uninitialize();
}
}
}
Loading

1 comment on commit d4e618c

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@check-spelling-bot Report

🔴 Please review

See the 📜action log for details.

Unrecognized words (131)
apidl
ari
arw
BESTEFFORT
BHIDSF
BVal
calpwstr
CARRAY
CElems
Chromakey
cidl
crw
CVal
DANGEROUSLYCOMMITMERELYTODISKCACHE
dcr
dcs
Dct
Dds
DELAYCREATION
drf
Dwma
eip
Excep
EXCEPINFO
EXTRINSICPROPERTIES
EXTRINSICPROPERTIESONLY
FASTPROPERTIESONLY
filetime
HANDLERPROPERTIESONLY
HDR
hicon
hif
HVal
IBitmap
IBlock
IColor
icolumn
IContext
IDecoder
IEncoder
IEnum
IIDI
iiq
IMetadata
IPalette
IQuery
IReader
ISource
ISurface
ithumbnail
jfi
jif
kdc
Keybd
Lcid
LOCKBYTES
LOCKTYPE
LVal
mdc
mef
mrw
neighborings
NOOPEN
nrw
ONLYIFCURRENT
ONLYONCE
OPENSLOWITEM
openspecs
OPLOCK
ori
overriden
pbgra
PBlob
pcch
pcelt
pcs
pef
PElems
Percision
pkey
ppenum
pprop
PREFERQUERYPROPERTIES
Previer
PRGBA
PROPERTYNOTFOUND
PROPVARIANT
pscid
psfi
pstatstg
pstm
pui
pvar
raf
retunred
rfid
RGBE
rgelt
rgf
rwl
rwz
sachaple
SAFEARRAY
SCID
Scode
Shcontf
SHELLDETAILS
Shgno
Softcoded
srf
SRGB
STGC
STGTY
Stroe
Strret
titlebar
tlbimp
toogle
UMsg
UOffset
USERDEFINED
UType
VARTYPE
VERSIONED
windowsapp
WMSDK
WReserved
WScan
wsp
WVk
YQuantized
Previously acknowledged words that are now absent brucelindbloom chromaticities companding DCR Eqn ffaa FILETIME HICON ITHUMBNAIL Pbgra PKEY Windowsapp :arrow_right:
To accept ✔️ these unrecognized words as correct and remove the previously acknowledged and now absent words, run the following commands

... in a clone of the git@github.com:microsoft/PowerToys.git repository
on the peek branch (ℹ️ how do I use this?):

curl -s -S -L 'https://raw.githubusercontent.com/check-spelling/check-spelling/v0.0.21/apply.pl' |
perl - 'https://github.com/microsoft/PowerToys/actions/runs/3643570918/attempts/1'
Available 📚 dictionaries could cover words not in the 📘 dictionary

This includes both expected items (2140) from .github/actions/spell-check/expect.txt and unrecognized words (131)

Dictionary Entries Covers
cspell:win32/src/win32.txt 53509 133
cspell:cpp/src/cpp.txt 30216 129
cspell:python/src/python/python-lib.txt 3873 31
cspell:php/php.txt 2597 17
cspell:node/node.txt 1768 14
cspell:typescript/typescript.txt 1211 12
cspell:java/java.txt 7642 11
cspell:python/src/python/python.txt 453 10
cspell:aws/aws.txt 218 8
cspell:r/src/r.txt 808 7

Consider adding them using (in .github/workflows/spelling2.yml):

      with:
        extra_dictionaries:
          cspell:win32/src/win32.txt
          cspell:cpp/src/cpp.txt
          cspell:python/src/python/python-lib.txt
          cspell:php/php.txt
          cspell:node/node.txt
          cspell:typescript/typescript.txt
          cspell:java/java.txt
          cspell:python/src/python/python.txt
          cspell:aws/aws.txt
          cspell:r/src/r.txt

To stop checking additional dictionaries, add:

      with:
        check_extra_dictionaries: ''
If the flagged items are 🤯 false positives

If items relate to a ...

  • binary file (or some other file you wouldn't want to check at all).

    Please add a file path to the excludes.txt file matching the containing file.

    File paths are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your files.

    ^ refers to the file's path from the root of the repository, so ^README\.md$ would exclude README.md (on whichever branch you're using).

  • well-formed pattern.

    If you can write a pattern that would match it,
    try adding it to the patterns.txt file.

    Patterns are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your lines.

    Note that patterns can't match multiline strings.

Please sign in to comment.