Skip to content

Commit

Permalink
Add Delete functionality for Peek.
Browse files Browse the repository at this point in the history
  • Loading branch information
daverayment committed Oct 13, 2024
1 parent 10b8687 commit 8859273
Show file tree
Hide file tree
Showing 11 changed files with 272 additions and 45 deletions.
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,12 @@
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="17.2.3" />
<PackageVersion Include="System.Management" Version="8.0.0" />
<PackageVersion Include="System.Reactive" Version="6.0.1" />
<PackageVersion Include="System.Runtime.Caching" Version="8.0.0" />
<PackageVersion Include="System.Runtime.Caching" Version="8.0.1" />
<!-- Package System.Security.Cryptography.ProtectedData added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Data.OleDb but the 8.0.1 version wasn't published to nuget. -->
<PackageVersion Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="8.0.0" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
<PackageVersion Include="System.Text.Json" Version="8.0.4" />
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
<PackageVersion Include="UnicodeInformation" Version="2.6.0" />
<PackageVersion Include="UnitsNet" Version="5.56.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
Expand Down
9 changes: 9 additions & 0 deletions src/modules/peek/Peek.FilePreviewer/FilePreview.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@
LoadingState="{x:Bind UnsupportedFilePreviewer.State, Mode=OneWay}"
Source="{x:Bind UnsupportedFilePreviewer.Preview, Mode=OneWay}"
Visibility="{x:Bind IsUnsupportedPreviewVisible(UnsupportedFilePreviewer, Previewer.State), Mode=OneWay}" />

<TextBlock
x:Name="NoMoreFiles"
Text="{x:Bind NoMoreFilesText, Mode=OneTime}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Style="{StaticResource BodyStrongTextBlockStyle}"
Visibility="Collapsed"
AutomationProperties.HeadingLevel="1" />
</Grid>
<UserControl.KeyboardAccelerators>
<KeyboardAccelerator
Expand Down
8 changes: 8 additions & 0 deletions src/modules/peek/Peek.FilePreviewer/FilePreview.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ public sealed partial class FilePreview : UserControl, IDisposable
typeof(FilePreview),
new PropertyMetadata(false, async (d, e) => await ((FilePreview)d).OnScalingFactorPropertyChanged()));

[ObservableProperty]
private int numberOfFiles;

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ImagePreviewer))]
[NotifyPropertyChangedFor(nameof(VideoPreviewer))]
Expand All @@ -62,6 +65,9 @@ public sealed partial class FilePreview : UserControl, IDisposable
[ObservableProperty]
private string infoTooltip = ResourceLoaderInstance.ResourceLoader.GetString("PreviewTooltip_Blank");

[ObservableProperty]
private string noMoreFilesText = ResourceLoaderInstance.ResourceLoader.GetString("NoMoreFiles");

private CancellationTokenSource _cancellationTokenSource = new();

public FilePreview()
Expand Down Expand Up @@ -158,6 +164,8 @@ private async Task OnItemPropertyChanged()
// Clear up any unmanaged resources before creating a new previewer instance.
(Previewer as IDisposable)?.Dispose();

NoMoreFiles.Visibility = NumberOfFiles == 0 ? Visibility.Visible : Visibility.Collapsed;

if (Item == null)
{
Previewer = null;
Expand Down
181 changes: 159 additions & 22 deletions src/modules/peek/Peek.UI/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,57 @@
// See the LICENSE file in the project root for more information.

using System;
using System.Linq;

using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using ManagedCommon;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Peek.Common.Helpers;
using Peek.Common.Models;
using Peek.UI.Models;
using Windows.Win32.Foundation;
using static Peek.UI.Native.NativeMethods;

namespace Peek.UI
{
public partial class MainWindowViewModel : ObservableObject
{
private static readonly string _defaultWindowTitle = ResourceLoaderInstance.ResourceLoader.GetString("AppTitle/Title");
/// <summary>
/// The minimum time in milliseconds between navigation events.
/// </summary>
private const int NavigationThrottleDelayMs = 100;

[ObservableProperty]
/// <summary>
/// The delay in milliseconds before a delete operation begins, to allow for navigation
/// away from the current item to occur.
/// </summary>
private const int DeleteDelayMs = 200;

/// <summary>
/// Holds the indexes of each <see cref="IFileSystemItem"/> the user has deleted.
/// </summary>
private readonly HashSet<int> _deletedItemIndexes = [];

private static readonly string _defaultWindowTitle = ResourceLoaderInstance.ResourceLoader.GetString("AppTitle/Title");

/// <summary>
/// The actual index of the current item in the items array. Does not necessarily
/// correspond to <see cref="_displayIndex"/> if one or more files have been deleted.
/// </summary>
private int _currentIndex;

/// <summary>
/// The item index to display in the titlebar.
/// </summary>
[ObservableProperty]
private int _displayIndex;

/// <summary>
/// The item to be displayed by a matching previewer. May be null if the user has deleted
/// all items.
/// </summary>
[ObservableProperty]
private IFileSystemItem? _currentItem;

Expand All @@ -37,11 +68,43 @@ partial void OnCurrentItemChanged(IFileSystemItem? value)
private string _windowTitle;

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(DisplayItemCount))]
private NeighboringItems? _items;

/// <summary>
/// The number of items selected and available to preview. Decreases as the user deletes
/// items. Displayed on the title bar.
/// </summary>
private int _displayItemCount;

public int DisplayItemCount
{
get => Items?.Count - _deletedItemIndexes.Count ?? 0;
set
{
if (_displayItemCount != value)
{
_displayItemCount = value;
OnPropertyChanged();
}
}
}

[ObservableProperty]
private double _scalingFactor = 1.0;

private enum NavigationDirection
{
Forwards,
Backwards,
}

/// <summary>
/// The current direction in which the user is moving through the items collection.
/// Determines how we act when a file is deleted.
/// </summary>
private NavigationDirection _navigationDirection = NavigationDirection.Forwards;

public NeighboringItemsQuery NeighboringItemsQuery { get; }

private DispatcherTimer NavigationThrottleTimer { get; set; } = new();
Expand All @@ -63,50 +126,124 @@ public void Initialize(HWND foregroundWindowHandle)
}
catch (Exception ex)
{
Logger.LogError("Failed to get File Explorer Items: " + ex.Message);
Logger.LogError("Failed to get File Explorer Items.", ex);
}

CurrentIndex = 0;
_currentIndex = DisplayIndex = 0;

if (Items != null && Items.Count > 0)
{
CurrentItem = Items[0];
}
CurrentItem = (Items != null && Items.Count > 0) ? Items[0] : null;
}

public void Uninitialize()
{
CurrentIndex = 0;
_currentIndex = DisplayIndex = 0;
CurrentItem = null;
_deletedItemIndexes.Clear();
Items = null;
_navigationDirection = NavigationDirection.Forwards;
}

public void AttemptPreviousNavigation()
public void AttemptPreviousNavigation() => Navigate(NavigationDirection.Backwards);

public void AttemptNextNavigation() => Navigate(NavigationDirection.Forwards);

private void Navigate(NavigationDirection direction, bool isAfterDelete = false)
{
if (NavigationThrottleTimer.IsEnabled)
{
return;
}

NavigationThrottleTimer.Start();
if (Items == null || Items.Count == _deletedItemIndexes.Count)
{
_currentIndex = DisplayIndex = 0;
CurrentItem = null;
return;
}

_navigationDirection = direction;

int offset = direction == NavigationDirection.Forwards ? 1 : -1;

do
{
_currentIndex = MathHelper.Modulo(_currentIndex + offset, Items.Count);
}
while (_deletedItemIndexes.Contains(_currentIndex));

CurrentItem = Items[_currentIndex];

// If we're navigating forwards after a delete operation, the displayed index does not
// change, e.g. "(2/3)" becomes "(2/2)".
if (isAfterDelete && direction == NavigationDirection.Forwards)
{
offset = 0;
}

DisplayIndex = MathHelper.Modulo(DisplayIndex + offset, DisplayItemCount);

var itemCount = Items?.Count ?? 1;
CurrentIndex = MathHelper.Modulo(CurrentIndex - 1, itemCount);
CurrentItem = Items?.ElementAtOrDefault(CurrentIndex);
NavigationThrottleTimer.Start();
}

public void AttemptNextNavigation()
/// <summary>
/// Sends the current item to the Recycle Bin.
/// </summary>
public void DeleteItem()
{
if (NavigationThrottleTimer.IsEnabled)
if (CurrentItem == null || !IsFilePath(CurrentItem.Path))
{
return;
}

NavigationThrottleTimer.Start();
_deletedItemIndexes.Add(_currentIndex);
OnPropertyChanged(nameof(DisplayItemCount));

string path = CurrentItem.Path;

DispatcherQueue.GetForCurrentThread().TryEnqueue(() =>
{
Task.Delay(DeleteDelayMs);
DeleteFile(path);
});

var itemCount = Items?.Count ?? 1;
CurrentIndex = MathHelper.Modulo(CurrentIndex + 1, itemCount);
CurrentItem = Items?.ElementAtOrDefault(CurrentIndex);
Navigate(_navigationDirection, isAfterDelete: true);
}

private void DeleteFile(string path, bool permanent = false)
{
SHFILEOPSTRUCT fileOp = new()
{
wFunc = FO_DELETE,
pFrom = path + "\0\0",
fFlags = (ushort)(FOF_NOCONFIRMATION | (permanent ? 0 : FOF_ALLOWUNDO)),
};

int result = SHFileOperation(ref fileOp);

if (result != 0)
{
string warning = "Could not delete file. " +
(DeleteFileErrors.TryGetValue(result, out string? errorMessage) ? errorMessage : $"Error code {result}.");
Logger.LogWarning(warning);
}
}

private static bool IsFilePath(string path)
{
if (string.IsNullOrEmpty(path))
{
return false;
}

try
{
FileAttributes attributes = File.GetAttributes(path);
return (attributes & FileAttributes.Directory) != FileAttributes.Directory;
}
catch (Exception)
{
return false;
}
}

private void NavigationThrottleTimer_Tick(object? sender, object e)
Expand Down
12 changes: 3 additions & 9 deletions src/modules/peek/Peek.UI/Models/NeighboringItems.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

namespace Peek.UI.Models
{
public class NeighboringItems : IReadOnlyList<IFileSystemItem>
public partial class NeighboringItems : IReadOnlyList<IFileSystemItem>
{
public IFileSystemItem this[int index] => Items[index] = Items[index] ?? ShellItemArray.GetItemAt(index).ToIFileSystemItem();

Expand All @@ -27,14 +27,8 @@ public NeighboringItems(IShellItemArray shellItemArray)
Items = new IFileSystemItem[Count];
}

public IEnumerator<IFileSystemItem> GetEnumerator()
{
return new NeighboringItemsEnumerator(this);
}
public IEnumerator<IFileSystemItem> GetEnumerator() => new NeighboringItemsEnumerator(this);

IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
50 changes: 49 additions & 1 deletion src/modules/peek/Peek.UI/Native/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;

using Peek.Common.Models;

namespace Peek.UI.Native
Expand Down Expand Up @@ -51,5 +51,53 @@ public enum AssocStr

[DllImport("user32.dll", CharSet = CharSet.Unicode)]
internal static extern int GetClassName(IntPtr hWnd, StringBuilder buf, int nMaxCount);

/// <summary>
/// Shell File Operations structure. Used for file deletion.
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
internal struct SHFILEOPSTRUCT
{
public IntPtr hwnd;
public int wFunc;
public string pFrom;
public string pTo;
public ushort fFlags;
public bool fAnyOperationsAborted;
public IntPtr hNameMappings;
public string lpszProgressTitle;
}

[DllImport("shell32.dll", CharSet = CharSet.Auto)]
internal static extern int SHFileOperation(ref SHFILEOPSTRUCT fileOp);

/// <summary>
/// File delete operation.
/// </summary>
internal const int FO_DELETE = 0x0003;

/// <summary>
/// Send to Recycle Bin flag.
/// </summary>
internal const int FOF_ALLOWUNDO = 0x0040;

/// <summary>
/// Do not request user confirmation for file delete flag.
/// </summary>
internal const int FOF_NOCONFIRMATION = 0x0010;

/// <summary>
/// Common error codes when calling SHFileOperation to delete a file.
/// </summary>
/// <remarks>See winerror.h for full list.</remarks>
public static readonly Dictionary<int, string> DeleteFileErrors = new()
{
{ 2, "The system cannot find the file specified." },
{ 3, "The system cannot find the path specified." },
{ 5, "Access is denied." },
{ 19, "The media is write protected." },
{ 32, "The process cannot access the file because it is being used by another process." },
{ 33, "The process cannot access the file because another process has locked a portion of the file." },
};
}
}
Loading

0 comments on commit 8859273

Please sign in to comment.