diff --git a/src/modules/peek/Peek.Common/Helpers/MathHelper.cs b/src/modules/peek/Peek.Common/Helpers/MathHelper.cs new file mode 100644 index 00000000000..e731d14030e --- /dev/null +++ b/src/modules/peek/Peek.Common/Helpers/MathHelper.cs @@ -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; + } + } +} diff --git a/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml.cs b/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml.cs index cab680dd1b9..499210284f3 100644 --- a/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml.cs +++ b/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml.cs @@ -16,8 +16,8 @@ using Peek.Common.Models; using Peek.FilePreviewer.Models; using Peek.FilePreviewer.Previewers; +using Peek.FilePreviewer.Previewers.Interfaces; using Windows.ApplicationModel.Resources; -using Windows.Foundation; namespace Peek.FilePreviewer { @@ -33,7 +33,7 @@ public sealed partial class FilePreview : UserControl nameof(Item), typeof(IFileSystemItem), typeof(FilePreview), - new PropertyMetadata(false, async (d, e) => await ((FilePreview)d).OnFilePropertyChanged())); + new PropertyMetadata(false, async (d, e) => await ((FilePreview)d).OnItemPropertyChanged())); public static readonly DependencyProperty ScalingFactorProperty = DependencyProperty.Register( @@ -80,12 +80,8 @@ private async void Previewer_PropertyChanged(object? sender, System.ComponentMod public IBrowserPreviewer? BrowserPreviewer => Previewer as IBrowserPreviewer; - public bool IsImageVisible => ImagePreviewer != null; - public IUnsupportedFilePreviewer? UnsupportedFilePreviewer => Previewer as IUnsupportedFilePreviewer; - public bool IsUnsupportedPreviewVisible => UnsupportedFilePreviewer != null; - public IFileSystemItem Item { get => (IFileSystemItem)GetValue(ItemProperty); @@ -117,7 +113,7 @@ public Visibility IsPreviewVisible(IPreviewer? previewer, PreviewState? state) return isValidPreview ? Visibility.Visible : Visibility.Collapsed; } - private async Task OnFilePropertyChanged() + private async Task OnItemPropertyChanged() { // Cancel previous loading task _cancellationTokenSource.Cancel(); @@ -258,11 +254,6 @@ private async Task UpdateImageTooltipAsync(CancellationToken cancellationToken) string dateModifiedFormatted = string.IsNullOrEmpty(dateModified) ? string.Empty : "\n" + ReadableStringHelper.FormatResourceString("PreviewTooltip_DateModified", dateModified); sb.Append(dateModifiedFormatted); - cancellationToken.ThrowIfCancellationRequested(); - Size? dimensions = await Task.Run(Item.GetImageSize); - string dimensionsFormatted = dimensions == null ? string.Empty : "\n" + ReadableStringHelper.FormatResourceString("PreviewTooltip_Dimensions", dimensions.Value.Width, dimensions.Value.Height); - sb.Append(dimensionsFormatted); - cancellationToken.ThrowIfCancellationRequested(); ulong bytes = await Task.Run(Item.GetSizeInBytes); string fileSize = ReadableStringHelper.BytesToReadableString(bytes); diff --git a/src/modules/peek/Peek.FilePreviewer/Peek.FilePreviewer.csproj b/src/modules/peek/Peek.FilePreviewer/Peek.FilePreviewer.csproj index 85025092672..2d24582e520 100644 --- a/src/modules/peek/Peek.FilePreviewer/Peek.FilePreviewer.csproj +++ b/src/modules/peek/Peek.FilePreviewer/Peek.FilePreviewer.csproj @@ -44,4 +44,8 @@ MSBuild:Compile + + + + diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/ImagePreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/ImagePreviewer.cs index c776bdefd62..0f60983be47 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/ImagePreviewer.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/ImagePreviewer.cs @@ -16,6 +16,7 @@ using Peek.Common.Helpers; using Peek.Common.Models; using Peek.FilePreviewer.Previewers.Helpers; +using Peek.FilePreviewer.Previewers.Interfaces; using Windows.Foundation; namespace Peek.FilePreviewer.Previewers diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/IBrowserPreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/IBrowserPreviewer.cs similarity index 100% rename from src/modules/peek/Peek.FilePreviewer/Previewers/IBrowserPreviewer.cs rename to src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/IBrowserPreviewer.cs diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/IImagePreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/IImagePreviewer.cs similarity index 85% rename from src/modules/peek/Peek.FilePreviewer/Previewers/IImagePreviewer.cs rename to src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/IImagePreviewer.cs index a326b0ee3c6..23fa1b08718 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/IImagePreviewer.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/IImagePreviewer.cs @@ -3,10 +3,9 @@ // See the LICENSE file in the project root for more information. using Microsoft.UI.Xaml.Media; -using Microsoft.UI.Xaml.Media.Imaging; using Windows.Foundation; -namespace Peek.FilePreviewer.Previewers +namespace Peek.FilePreviewer.Previewers.Interfaces { public interface IImagePreviewer : IPreviewer { diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/IPreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/IPreviewer.cs similarity index 100% rename from src/modules/peek/Peek.FilePreviewer/Previewers/IPreviewer.cs rename to src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/IPreviewer.cs diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/IUnsupportedFilePreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/IUnsupportedFilePreviewer.cs similarity index 100% rename from src/modules/peek/Peek.FilePreviewer/Previewers/IUnsupportedFilePreviewer.cs rename to src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/IUnsupportedFilePreviewer.cs diff --git a/src/modules/peek/Peek.UI/App.xaml.cs b/src/modules/peek/Peek.UI/App.xaml.cs index 5c518445e0f..26c52227f21 100644 --- a/src/modules/peek/Peek.UI/App.xaml.cs +++ b/src/modules/peek/Peek.UI/App.xaml.cs @@ -4,7 +4,11 @@ using System; using ManagedCommon; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.UI.Xaml; +using Peek.FilePreviewer; +using Peek.UI.Views; using WinUIEx; namespace Peek.UI @@ -16,6 +20,13 @@ public partial class App : Application { public static int PowerToysPID { get; set; } + public IHost Host + { + get; + } + + private Window? Window { get; set; } + /// /// Initializes a new instance of the class. /// Initializes the singleton application object. This is the first line of authored code @@ -23,7 +34,35 @@ public partial class App : Application /// public App() { - this.InitializeComponent(); + InitializeComponent(); + + Host = Microsoft.Extensions.Hosting.Host. + CreateDefaultBuilder(). + UseContentRoot(AppContext.BaseDirectory). + ConfigureServices((context, services) => + { + // Core Services + services.AddTransient(); + + // Views and ViewModels + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + }). + Build(); + + UnhandledException += App_UnhandledException; + } + + public static T GetService() + 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; } /// @@ -44,12 +83,14 @@ protected override void OnLaunched(LaunchActivatedEventArgs args) } } - window = new MainWindow(); + Window = new MainWindow(); - window.Activate(); - window.Hide(); + Window.Activate(); + Window.Hide(); } - private Window? window; + private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e) + { + } } } diff --git a/src/modules/peek/Peek.UI/Extensions/IShellItemExtensions.cs b/src/modules/peek/Peek.UI/Extensions/IShellItemExtensions.cs new file mode 100644 index 00000000000..8c95edb9a31 --- /dev/null +++ b/src/modules/peek/Peek.UI/Extensions/IShellItemExtensions.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +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) + { + // TODO: Handle cases that do not have a file system path like Recycle Bin. + } + + return File.Exists(path) ? new FileItem(path) : new FolderItem(path); + } + } +} diff --git a/src/modules/peek/Peek.UI/FolderItemsQuery.cs b/src/modules/peek/Peek.UI/FolderItemsQuery.cs deleted file mode 100644 index 96f90023ff6..00000000000 --- a/src/modules/peek/Peek.UI/FolderItemsQuery.cs +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using Microsoft.UI.Dispatching; -using Peek.Common.Models; -using Peek.UI.Helpers; - -namespace Peek.UI -{ - public partial class FolderItemsQuery : ObservableObject - { - private const int UninitializedItemIndex = -1; - private readonly object _mutateQueryDataLock = new(); - private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); - - [ObservableProperty] - private IFileSystemItem? currentFile; - - [ObservableProperty] - private List files = new(); - - [ObservableProperty] - private bool isMultiSelection; - - [ObservableProperty] - private int currentItemIndex = UninitializedItemIndex; - - private CancellationTokenSource CancellationTokenSource { get; set; } = new CancellationTokenSource(); - - private Task? InitializeFilesTask { get; set; } = null; - - public void Clear() - { - CurrentFile = null; - IsMultiSelection = false; - - if (InitializeFilesTask != null && InitializeFilesTask.Status == TaskStatus.Running) - { - Debug.WriteLine("Detected existing initializeFilesTask running. Cancelling it.."); - CancellationTokenSource.Cancel(); - } - - InitializeFilesTask = null; - - lock (_mutateQueryDataLock) - { - _dispatcherQueue.TryEnqueue(() => - { - Files = new List(); - 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 selectedItems = FileExplorerHelper.GetSelectedItems(); - if (!selectedItems.Any()) - { - return; - } - - bool hasMoreThanOneItem = selectedItems.Skip(1).Any(); - IsMultiSelection = hasMoreThanOneItem; - - // Prioritize setting CurrentFile, which notifies UI - var firstSelectedItem = selectedItems.First(); - CurrentFile = firstSelectedItem; - - // TODO: we shouldn't get all files from the SHell API, we should query them - var items = hasMoreThanOneItem ? selectedItems : FileExplorerHelper.GetItems(); - 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( - IEnumerable items, - IFileSystemItem firstSelectedItem, - CancellationToken cancellationToken) - { - var listOfItems = items.ToList(); - var currentItemIndex = listOfItems.FindIndex(item => item.Path == firstSelectedItem.Path); - - if (currentItemIndex < 0) - { - Debug.WriteLine("File query initialization: selectedItem index not found. Navigation remains disabled."); - return; - } - - cancellationToken.ThrowIfCancellationRequested(); - - lock (_mutateQueryDataLock) - { - cancellationToken.ThrowIfCancellationRequested(); - - _dispatcherQueue.TryEnqueue(() => - { - Files = listOfItems; - CurrentItemIndex = currentItemIndex; - }); - } - } - } -} diff --git a/src/modules/peek/Peek.UI/Helpers/FileExplorerHelper.cs b/src/modules/peek/Peek.UI/Helpers/FileExplorerHelper.cs index 37f0d1087f3..4f361a8dce3 100644 --- a/src/modules/peek/Peek.UI/Helpers/FileExplorerHelper.cs +++ b/src/modules/peek/Peek.UI/Helpers/FileExplorerHelper.cs @@ -3,34 +3,30 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; -using System.IO; using Peek.Common.Models; using Peek.UI.Extensions; using SHDocVw; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.UI.Shell; -using static System.Runtime.InteropServices.JavaScript.JSType; using IServiceProvider = Peek.Common.Models.IServiceProvider; namespace Peek.UI.Helpers { public static class FileExplorerHelper { - public static IEnumerable GetSelectedItems() + internal static IShellItemArray? GetSelectedItems(HWND foregroundWindowHandle) { - return GetItemsInternal(onlySelectedFiles: true); + return GetItemsInternal(foregroundWindowHandle, onlySelectedFiles: true); } - public static IEnumerable GetItems() + internal static IShellItemArray? GetItems(HWND foregroundWindowHandle) { - return GetItemsInternal(onlySelectedFiles: false); + return GetItemsInternal(foregroundWindowHandle, onlySelectedFiles: false); } - private static IEnumerable GetItemsInternal(bool onlySelectedFiles) + private static IShellItemArray? GetItemsInternal(HWND foregroundWindowHandle, bool onlySelectedFiles) { - var foregroundWindowHandle = PInvoke.GetForegroundWindow(); if (foregroundWindowHandle.IsDesktopWindow()) { return GetItemsFromDesktop(foregroundWindowHandle, onlySelectedFiles); @@ -41,31 +37,28 @@ private static IEnumerable GetItemsInternal(bool onlySelectedFi } } - private static IEnumerable GetItemsFromDesktop(HWND foregroundWindowHandle, bool 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); - 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); - if (items is IShellItemArray array) - { - return array.ToEnumerable(); - } - return new List(); + IShellItemArray? shellItemArray = GetShellItemArray(shellBrowser, onlySelectedFiles); + return shellItemArray; } - private static IEnumerable GetItemsFromFileExplorer(HWND foregroundWindowHandle, bool onlySelectedFiles) + private static IShellItemArray? GetItemsFromFileExplorer(HWND foregroundWindowHandle, bool onlySelectedFiles) { + IShellItemArray? shellItemArray = null; + var activeTab = foregroundWindowHandle.GetActiveTab(); var shell = new Shell32.Shell(); @@ -83,41 +76,21 @@ private static IEnumerable GetItemsFromFileExplorer(HWND foregr if (activeTab == shellBrowserHandle) { - var items = onlySelectedFiles ? shellFolderView.SelectedItems() : shellFolderView.Folder?.Items(); - return items != null ? items.ToEnumerable() : new List(); + shellItemArray = GetShellItemArray(shellBrowser, onlySelectedFiles); + return shellItemArray; } } } - return new List(); - } - - private static IEnumerable ToEnumerable(this IShellItemArray array) - { - for (var i = 0; i < array.GetCount(); i++) - { - IShellItem item = array.GetItemAt(i); - string path = string.Empty; - try - { - path = item.GetDisplayName(SIGDN.SIGDN_FILESYSPATH); - } - catch (Exception) - { - // TODO: Handle cases that do not have a file system path like Recycle Bin. - } - - yield return File.Exists(path) ? new FileItem(path) : new FolderItem(path); - } + return shellItemArray; } - private static IEnumerable ToEnumerable(this Shell32.FolderItems folderItems) + private static IShellItemArray? GetShellItemArray(IShellBrowser shellBrowser, bool onlySelectedFiles) { - foreach (Shell32.FolderItem item in folderItems) - { - // TODO: Handle cases where it is neither a file or a folder - yield return File.Exists(item.Path) ? new FileItem(item.Path) : new FolderItem(item.Path); - } + 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; } } } diff --git a/src/modules/peek/Peek.UI/MainWindow.xaml b/src/modules/peek/Peek.UI/MainWindow.xaml index 2456a67fb4f..a1c9fda9cd5 100644 --- a/src/modules/peek/Peek.UI/MainWindow.xaml +++ b/src/modules/peek/Peek.UI/MainWindow.xaml @@ -29,15 +29,15 @@ + 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}" /> + Item="{x:Bind ViewModel.CurrentItem, Mode=OneWay}" + PreviewSizeChanged="FilePreviewer_PreviewSizeChanged" + ScalingFactor="{x:Bind ViewModel.ScalingFactor, Mode=OneWay}" /> diff --git a/src/modules/peek/Peek.UI/MainWindow.xaml.cs b/src/modules/peek/Peek.UI/MainWindow.xaml.cs index d02481bd710..0d057d624ee 100644 --- a/src/modules/peek/Peek.UI/MainWindow.xaml.cs +++ b/src/modules/peek/Peek.UI/MainWindow.xaml.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Linq; using interop; +using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.UI.Windowing; using Microsoft.UI.Xaml.Input; using Peek.Common.Constants; @@ -22,11 +23,13 @@ namespace Peek.UI /// public sealed partial class MainWindow : WindowEx { + public MainWindowViewModel ViewModel { get; } + public MainWindow() { InitializeComponent(); - ViewModel = new MainWindowViewModel(); + ViewModel = App.GetService(); NativeEventWaiter.WaitForEventLoop(Constants.ShowPeekEvent(), OnPeekHotkey); @@ -35,8 +38,6 @@ public MainWindow() AppWindow.Closing += AppWindow_Closing; } - public MainWindowViewModel ViewModel { get; } - /// /// Handle Peek hotkey, by toggling the window visibility and querying files when necessary. /// @@ -71,7 +72,7 @@ private void RightNavigationInvoked(KeyboardAccelerator sender, KeyboardAccelera private void Initialize() { - ViewModel.FolderItemsQuery.Start(); + ViewModel.Initialize(); ViewModel.ScalingFactor = this.GetMonitorScale(); } @@ -80,8 +81,8 @@ private void Uninitialize() this.Restore(); this.Hide(); - // TODO: move into general ViewModel method when needed - ViewModel.FolderItemsQuery.Clear(); + ViewModel.Uninitialize(); + ViewModel.ScalingFactor = 1; } /// @@ -132,15 +133,18 @@ private void AppWindow_Closing(AppWindow sender, AppWindowClosingEventArgs args) private bool IsNewSingleSelectedItem() { - var selectedItems = FileExplorerHelper.GetSelectedItems(); - if (!selectedItems.Any() || selectedItems.Skip(1).Any()) + 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.First().Path; - var currentFilePath = ViewModel.FolderItemsQuery.CurrentFile?.Path; - if (fileExplorerSelectedItemPath == null || currentFilePath == null || fileExplorerSelectedItemPath == currentFilePath) + var fileExplorerSelectedItemPath = selectedItems.GetItemAt(0).ToIFileSystemItem().Path; + var currentItemPath = ViewModel.CurrentItem?.Path; + if (fileExplorerSelectedItemPath == null || currentItemPath == null || fileExplorerSelectedItemPath == currentItemPath) { return false; } diff --git a/src/modules/peek/Peek.UI/MainWindowViewModel.cs b/src/modules/peek/Peek.UI/MainWindowViewModel.cs index 8f385b29967..61f8bcbc384 100644 --- a/src/modules/peek/Peek.UI/MainWindowViewModel.cs +++ b/src/modules/peek/Peek.UI/MainWindowViewModel.cs @@ -3,8 +3,12 @@ // 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 { @@ -12,12 +16,44 @@ public partial class MainWindowViewModel : ObservableObject { private const int NavigationThrottleDelayMs = 100; - public MainWindowViewModel() + [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; + CurrentItem = Items?.FirstOrDefault(); + } + + public void Uninitialize() + { + CurrentIndex = 0; + CurrentItem = null; + Items = null; + } + public void AttemptLeftNavigation() { if (NavigationThrottleTimer.IsEnabled) @@ -27,8 +63,9 @@ public void AttemptLeftNavigation() NavigationThrottleTimer.Start(); - // TODO: return a bool so UI can give feedback in case navigation is unavailable - FolderItemsQuery.UpdateCurrentItemIndex(FolderItemsQuery.CurrentItemIndex - 1); + var itemCount = Items?.Count ?? 1; + CurrentIndex = MathHelper.Modulo(CurrentIndex - 1, itemCount); + CurrentItem = Items?.ElementAtOrDefault(CurrentIndex); } public void AttemptRightNavigation() @@ -40,8 +77,9 @@ public void AttemptRightNavigation() NavigationThrottleTimer.Start(); - // TODO: return a bool so UI can give feedback in case navigation is unavailable - FolderItemsQuery.UpdateCurrentItemIndex(FolderItemsQuery.CurrentItemIndex + 1); + var itemCount = Items?.Count ?? 1; + CurrentIndex = MathHelper.Modulo(CurrentIndex + 1, itemCount); + CurrentItem = Items?.ElementAtOrDefault(CurrentIndex); } private void NavigationThrottleTimer_Tick(object? sender, object e) @@ -53,13 +91,5 @@ private void NavigationThrottleTimer_Tick(object? sender, object e) ((DispatcherTimer)sender).Stop(); } - - [ObservableProperty] - private FolderItemsQuery _folderItemsQuery = new(); - - [ObservableProperty] - private double _scalingFactor = 1.0; - - private DispatcherTimer NavigationThrottleTimer { get; set; } = new(); } } diff --git a/src/modules/peek/Peek.UI/Models/NeighboringItems.cs b/src/modules/peek/Peek.UI/Models/NeighboringItems.cs new file mode 100644 index 00000000000..9fc65f50a4a --- /dev/null +++ b/src/modules/peek/Peek.UI/Models/NeighboringItems.cs @@ -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 + { + 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 GetEnumerator() + { + return new NeighboringItemsEnumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/src/modules/peek/Peek.UI/Models/NeighboringItemsEnumerator.cs b/src/modules/peek/Peek.UI/Models/NeighboringItemsEnumerator.cs new file mode 100644 index 00000000000..a0f2fe82960 --- /dev/null +++ b/src/modules/peek/Peek.UI/Models/NeighboringItemsEnumerator.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; +using Peek.Common.Models; + +namespace Peek.UI.Models +{ + public class NeighboringItemsEnumerator : IEnumerator + { + 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() + { + } + + public bool MoveNext() + { + if (CurrentIndex >= Items.Count) + { + return false; + } + + CurrentIndex++; + + return true; + } + + public void Reset() + { + CurrentIndex = -1; + } + } +} diff --git a/src/modules/peek/Peek.UI/Peek.UI.csproj b/src/modules/peek/Peek.UI/Peek.UI.csproj index 3c30f9c3695..e6b90921be8 100644 --- a/src/modules/peek/Peek.UI/Peek.UI.csproj +++ b/src/modules/peek/Peek.UI/Peek.UI.csproj @@ -67,6 +67,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/modules/peek/Peek.UI/Services/NeighboringItemsQuery.cs b/src/modules/peek/Peek.UI/Services/NeighboringItemsQuery.cs new file mode 100644 index 00000000000..78d0f97a5d4 --- /dev/null +++ b/src/modules/peek/Peek.UI/Services/NeighboringItemsQuery.cs @@ -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); + } + } +}