Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Delete functionality for Peek #35418

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
96 changes: 0 additions & 96 deletions Directory.Packages.props

This file was deleted.

10 changes: 10 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,16 @@
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:Uid="NoMoreFiles"
x:Name="NoMoreFiles"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{StaticResource TextFillColorSecondaryBrush}"
Visibility="Collapsed"
daverayment marked this conversation as resolved.
Show resolved Hide resolved
TextWrapping="Wrap"
AutomationProperties.HeadingLevel="1" />
</Grid>
<UserControl.KeyboardAccelerators>
<KeyboardAccelerator
Expand Down
5 changes: 5 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 Down Expand Up @@ -158,6 +161,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))
daverayment marked this conversation as resolved.
Show resolved Hide resolved
{
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)),
Copy link
Collaborator

@htcfreek htcfreek Oct 14, 2024

Choose a reason for hiding this comment

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

Does this override the recycle bin property? (It is possible to configure the delete confirmation in the recycle bin properties.)

How does Fotos app handle it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This replicates how the Photos application handles deletes. Pressing Delete immediately sends the file to the Recycle Bin without popping up a message, and then it moves on to the next file.

The permanent argument is there for possible future use, but is always false for now.

};

int result = SHFileOperation(ref fileOp);

if (result != 0)
daverayment marked this conversation as resolved.
Show resolved Hide resolved
{
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
Loading
Loading