mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-15 11:17:53 +01:00
[Peek]Add Delete functionality (#35418)
* Add Delete functionality for Peek. * Updated the "No More Files" text block to use a Uid to load its resource text. Also altered the text style to be consistent with the FailedFallbackPreviewControl error page. * Revert "Delete Directory.Packages.props" This reverts commit 3a10918c9f91de64785722e4bdb33c58d1c2daea. * Attempt to appease the spell-checking bot by renaming flag const. * Show error message InfoBar if file deletion failed. * Resolve XAML styling. * XAML styling fix. * Settings app updates for new delete confirmation setting. * Add delete confirmation dialog and settings to Peek. Add shell notification event after delete operation. * Spelling updates. * Spelling update. * Remove permanent delete parameter, YAGNI. Add hwnd parameter to delete so warning dialogs are correctly parented. Fix flags to not hide permanent delete warning. * Simplify delete confirmation dialog. Remove workaround for focus visual issue. Ensure delete confirmation dialog is closed when the main window visibility is toggled. * Fix delete delay. Do not regard user cancellations of permanent deletes as an error, but log them as info anyway. More descriptive name for delete confirmation dialog checkbox. * Fix multiple Content_KeyUp events being raised for MainWindow. * Synchronise ConfirmFileDelete setting between Peek and Settings app. * Update following review: split System usings from others; do not log deleted item name. * Fix XAML style
This commit is contained in:
3
.github/actions/spell-check/expect.txt
vendored
3
.github/actions/spell-check/expect.txt
vendored
@@ -490,6 +490,7 @@ flac
|
||||
flyouts
|
||||
FMask
|
||||
FOF
|
||||
WANTNUKEWARNING
|
||||
FOFX
|
||||
FOLDERID
|
||||
folderpath
|
||||
@@ -1418,6 +1419,7 @@ SHCNE
|
||||
SHCNF
|
||||
SHCONTF
|
||||
Shcore
|
||||
shellapi
|
||||
SHELLDETAILS
|
||||
SHELLDLL
|
||||
shellex
|
||||
@@ -1812,6 +1814,7 @@ windowssearch
|
||||
windowssettings
|
||||
WINDOWSTYLES
|
||||
WINDOWSTYLESICON
|
||||
winerror
|
||||
WINEVENT
|
||||
winget
|
||||
wingetcreate
|
||||
|
||||
@@ -98,4 +98,4 @@
|
||||
<PackageVersion Include="Microsoft.VariantAssignment.Client" Version="2.4.17140001" />
|
||||
<PackageVersion Include="Microsoft.VariantAssignment.Contract" Version="3.0.16990001" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -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:Name="NoMoreFiles"
|
||||
x:Uid="NoMoreFiles"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.HeadingLevel="1"
|
||||
Foreground="{StaticResource TextFillColorSecondaryBrush}"
|
||||
TextWrapping="Wrap"
|
||||
Visibility="Collapsed" />
|
||||
</Grid>
|
||||
<UserControl.KeyboardAccelerators>
|
||||
<KeyboardAccelerator
|
||||
|
||||
@@ -47,6 +47,9 @@ namespace Peek.FilePreviewer
|
||||
typeof(FilePreview),
|
||||
new PropertyMetadata(false, async (d, e) => await ((FilePreview)d).OnScalingFactorPropertyChanged()));
|
||||
|
||||
[ObservableProperty]
|
||||
private int numberOfFiles;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ImagePreviewer))]
|
||||
[NotifyPropertyChangedFor(nameof(VideoPreviewer))]
|
||||
@@ -62,6 +65,9 @@ namespace Peek.FilePreviewer
|
||||
[ObservableProperty]
|
||||
private string infoTooltip = ResourceLoaderInstance.ResourceLoader.GetString("PreviewTooltip_Blank");
|
||||
|
||||
[ObservableProperty]
|
||||
private string noMoreFilesText = ResourceLoaderInstance.ResourceLoader.GetString("NoMoreFiles");
|
||||
|
||||
private CancellationTokenSource _cancellationTokenSource = new();
|
||||
|
||||
public FilePreview()
|
||||
@@ -158,6 +164,8 @@ namespace Peek.FilePreviewer
|
||||
// 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;
|
||||
|
||||
100
src/modules/peek/Peek.UI/Helpers/DeleteErrorMessageHelper.cs
Normal file
100
src/modules/peek/Peek.UI/Helpers/DeleteErrorMessageHelper.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
// 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.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using ManagedCommon;
|
||||
using static Peek.Common.Helpers.ResourceLoaderInstance;
|
||||
|
||||
namespace Peek.UI.Helpers;
|
||||
|
||||
public static class DeleteErrorMessageHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// The "Could not delete 'filename'." message, which begins every user-facing error string.
|
||||
/// </summary>
|
||||
private static readonly CompositeFormat UserMessagePrefix =
|
||||
CompositeFormat.Parse(ResourceLoader.GetString("DeleteFileError_Prefix") + " ");
|
||||
|
||||
/// <summary>
|
||||
/// The message displayed if the delete failed but the error code isn't covered in the
|
||||
/// <see cref="DeleteFileErrors"/> collection.
|
||||
/// </summary>
|
||||
private static readonly string GenericErrorMessage = ResourceLoader.GetString("DeleteFileError_Generic");
|
||||
|
||||
/// <summary>
|
||||
/// The collection of the most common error codes with their matching log messages and user-
|
||||
/// facing descriptions.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<int, (string LogMessage, string UserMessage)> DeleteFileErrors = new()
|
||||
{
|
||||
{
|
||||
2,
|
||||
(
|
||||
"The system cannot find the file specified.",
|
||||
ResourceLoader.GetString("DeleteFileError_NotFound")
|
||||
)
|
||||
},
|
||||
{
|
||||
3,
|
||||
(
|
||||
"The system cannot find the path specified.",
|
||||
ResourceLoader.GetString("DeleteFileError_NotFound")
|
||||
)
|
||||
},
|
||||
{
|
||||
5,
|
||||
(
|
||||
"Access is denied.",
|
||||
ResourceLoader.GetString("DeleteFileError_AccessDenied")
|
||||
)
|
||||
},
|
||||
{
|
||||
19,
|
||||
(
|
||||
"The media is write protected.",
|
||||
ResourceLoader.GetString("DeleteFileError_WriteProtected")
|
||||
)
|
||||
},
|
||||
{
|
||||
32,
|
||||
(
|
||||
"The process cannot access the file because it is being used by another process.",
|
||||
ResourceLoader.GetString("DeleteFileError_FileInUse")
|
||||
)
|
||||
},
|
||||
{
|
||||
33,
|
||||
(
|
||||
"The process cannot access the file because another process has locked a portion of the file.",
|
||||
ResourceLoader.GetString("DeleteFileError_FileInUse")
|
||||
)
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Logs an error message in response to a failed file deletion attempt.
|
||||
/// </summary>
|
||||
/// <param name="errorCode">The error code returned from the delete call.</param>
|
||||
public static void LogError(int errorCode) =>
|
||||
Logger.LogError(DeleteFileErrors.TryGetValue(errorCode, out var messages) ?
|
||||
messages.LogMessage :
|
||||
$"Error {errorCode} occurred while deleting the file.");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the message to display in the UI for a specific delete error code.
|
||||
/// </summary>
|
||||
/// <param name="filename">The name of the file which could not be deleted.</param>
|
||||
/// <param name="errorCode">The error code result from the delete call.</param>
|
||||
/// <returns>A string containing the message to show in the user interface.</returns>
|
||||
public static string GetUserErrorMessage(string filename, int errorCode)
|
||||
{
|
||||
string prefix = string.Format(CultureInfo.InvariantCulture, UserMessagePrefix, filename);
|
||||
|
||||
return DeleteFileErrors.TryGetValue(errorCode, out var messages) ?
|
||||
prefix + messages.UserMessage :
|
||||
prefix + GenericErrorMessage;
|
||||
}
|
||||
}
|
||||
@@ -3,26 +3,62 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Peek.Common.Extensions;
|
||||
using Peek.Common.Helpers;
|
||||
using Peek.Common.Models;
|
||||
using Peek.UI.Helpers;
|
||||
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;
|
||||
|
||||
@@ -37,11 +73,49 @@ namespace Peek.UI
|
||||
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;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _errorMessage = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isErrorVisible = false;
|
||||
|
||||
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();
|
||||
@@ -63,50 +137,215 @@ namespace Peek.UI
|
||||
}
|
||||
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;
|
||||
IsErrorVisible = false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
var itemCount = Items?.Count ?? 1;
|
||||
CurrentIndex = MathHelper.Modulo(CurrentIndex - 1, itemCount);
|
||||
CurrentItem = Items?.ElementAtOrDefault(CurrentIndex);
|
||||
_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);
|
||||
|
||||
NavigationThrottleTimer.Start();
|
||||
}
|
||||
|
||||
public void AttemptNextNavigation()
|
||||
/// <summary>
|
||||
/// Sends the current item to the Recycle Bin.
|
||||
/// </summary>
|
||||
/// <param name="skipConfirmationChecked">The IsChecked property of the "Don't ask me
|
||||
/// again" checkbox on the delete confirmation dialog.</param>
|
||||
public void DeleteItem(bool? skipConfirmationChecked, nint hwnd)
|
||||
{
|
||||
if (NavigationThrottleTimer.IsEnabled)
|
||||
if (CurrentItem == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NavigationThrottleTimer.Start();
|
||||
bool skipConfirmation = skipConfirmationChecked ?? false;
|
||||
bool shouldShowConfirmation = !skipConfirmation;
|
||||
Application.Current.GetService<IUserSettings>().ConfirmFileDelete = shouldShowConfirmation;
|
||||
|
||||
var itemCount = Items?.Count ?? 1;
|
||||
CurrentIndex = MathHelper.Modulo(CurrentIndex + 1, itemCount);
|
||||
CurrentItem = Items?.ElementAtOrDefault(CurrentIndex);
|
||||
var item = CurrentItem;
|
||||
|
||||
if (File.Exists(item.Path) && !IsFilePath(item.Path))
|
||||
{
|
||||
// The path is to a folder, not a file, or its attributes could not be retrieved.
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the file count and total files.
|
||||
int index = _currentIndex;
|
||||
_deletedItemIndexes.Add(index);
|
||||
OnPropertyChanged(nameof(DisplayItemCount));
|
||||
|
||||
// Attempt the deletion then navigate to the next file.
|
||||
DispatcherQueue.GetForCurrentThread().TryEnqueue(async () =>
|
||||
{
|
||||
await Task.Delay(DeleteDelayMs);
|
||||
int result = DeleteFile(item, hwnd);
|
||||
|
||||
if (result == 0)
|
||||
{
|
||||
// Success.
|
||||
return;
|
||||
}
|
||||
|
||||
if (result == ERROR_CANCELLED)
|
||||
{
|
||||
if (Path.GetPathRoot(item.Path) is string root)
|
||||
{
|
||||
var driveInfo = new DriveInfo(root);
|
||||
Logger.LogInfo($"User cancelled item deletion on {driveInfo.DriveType} drive.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// For failures other than user cancellation, log the error and show a message
|
||||
// in the UI.
|
||||
DeleteErrorMessageHelper.LogError(result);
|
||||
ShowDeleteError(item.Name, result);
|
||||
}
|
||||
|
||||
// For all errors, reinstate the deleted file if it still exists.
|
||||
ReinstateDeletedFile(item, index);
|
||||
});
|
||||
|
||||
Navigate(_navigationDirection, isAfterDelete: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete a file by moving it to the Recycle Bin. Refresh any shell listeners.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to delete.</param>
|
||||
/// <param name="hwnd">The handle of the main window.</param>
|
||||
/// <returns>The result of the file operation call. A non-zero result indicates failure.
|
||||
/// </returns>
|
||||
private int DeleteFile(IFileSystemItem item, nint hwnd)
|
||||
{
|
||||
// Move to the Recycle Bin and warn about permanent deletes.
|
||||
var flags = (ushort)(FOF_ALLOWUNDO | FOF_WANTNUKEWARNING);
|
||||
|
||||
SHFILEOPSTRUCT fileOp = new()
|
||||
{
|
||||
wFunc = FO_DELETE,
|
||||
pFrom = item.Path + "\0\0", // Path arguments must be double null-terminated.
|
||||
fFlags = flags,
|
||||
hwnd = hwnd,
|
||||
};
|
||||
|
||||
int result = SHFileOperation(ref fileOp);
|
||||
if (result == 0)
|
||||
{
|
||||
SendDeleteChangeNotification(item.Path);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void ReinstateDeletedFile(IFileSystemItem item, int index)
|
||||
{
|
||||
if (File.Exists(item.Path))
|
||||
{
|
||||
_deletedItemIndexes.Remove(index);
|
||||
OnPropertyChanged(nameof(DisplayItemCount));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Informs shell listeners like Explorer windows that a delete operation has occurred.
|
||||
/// </summary>
|
||||
/// <param name="path">Full path to the file which was deleted.</param>
|
||||
private void SendDeleteChangeNotification(string path)
|
||||
{
|
||||
IntPtr pathPtr = Marshal.StringToHGlobalUni(path);
|
||||
try
|
||||
{
|
||||
if (pathPtr == IntPtr.Zero)
|
||||
{
|
||||
Logger.LogError("Could not allocate memory for path string.");
|
||||
}
|
||||
else
|
||||
{
|
||||
SHChangeNotify(SHCNE_DELETE, SHCNF_PATH, pathPtr, IntPtr.Zero);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(pathPtr);
|
||||
}
|
||||
}
|
||||
|
||||
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 ShowDeleteError(string filename, int errorCode)
|
||||
{
|
||||
IsErrorVisible = false;
|
||||
ErrorMessage = DeleteErrorMessageHelper.GetUserErrorMessage(filename, errorCode);
|
||||
IsErrorVisible = true;
|
||||
}
|
||||
|
||||
private void NavigationThrottleTimer_Tick(object? sender, object e)
|
||||
|
||||
@@ -27,14 +27,8 @@ namespace Peek.UI.Models
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -51,5 +51,85 @@ namespace Peek.UI.Native
|
||||
|
||||
[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.Unicode)]
|
||||
internal struct SHFILEOPSTRUCT
|
||||
{
|
||||
public IntPtr hwnd;
|
||||
public uint wFunc;
|
||||
public string pFrom;
|
||||
public string pTo;
|
||||
public ushort fFlags;
|
||||
public bool fAnyOperationsAborted;
|
||||
public IntPtr hNameMappings;
|
||||
public string lpszProgressTitle;
|
||||
}
|
||||
|
||||
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
|
||||
internal static extern int SHFileOperation(ref SHFILEOPSTRUCT fileOp);
|
||||
|
||||
/// <summary>
|
||||
/// File delete operation.
|
||||
/// </summary>
|
||||
internal const uint FO_DELETE = 0x0003;
|
||||
|
||||
/// <summary>
|
||||
/// Send to Recycle Bin flag.
|
||||
/// </summary>
|
||||
internal const ushort FOF_ALLOWUNDO = 0x0040;
|
||||
|
||||
/// <summary>
|
||||
/// Do not request user confirmation for file deletes.
|
||||
/// </summary>
|
||||
internal const ushort FOF_NO_CONFIRMATION = 0x0010;
|
||||
|
||||
/// <summary>
|
||||
/// Warn if a file cannot be recycled and would instead be permanently deleted. (Partially
|
||||
/// overrides FOF_NO_CONFIRMATION.) This can be tested by attempting to delete a file on a
|
||||
/// FAT volume, e.g. a USB key.
|
||||
/// </summary>
|
||||
/// <remarks>Declared in shellapi.h./remarks>
|
||||
internal const ushort FOF_WANTNUKEWARNING = 0x4000;
|
||||
|
||||
/// <summary>
|
||||
/// The user cancelled the delete operation. Not classified as an error for our purposes.
|
||||
/// </summary>
|
||||
internal const int ERROR_CANCELLED = 1223;
|
||||
|
||||
/// <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." },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Shell Change Notify. Used to inform shell listeners after we've completed a file
|
||||
/// operation like Delete or Move.
|
||||
/// </summary>
|
||||
[DllImport("shell32.dll", CharSet = CharSet.Auto)]
|
||||
internal static extern void SHChangeNotify(uint wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2);
|
||||
|
||||
/// <summary>
|
||||
/// File System Notification Flag, indicating that the operation was a file deletion.
|
||||
/// </summary>
|
||||
/// <remarks>See ShlObj_core.h for constant definitions.</remarks>
|
||||
internal const uint SHCNE_DELETE = 0x00000004;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that SHChangeNotify's dwItem1 and (optionally) dwItem2 parameters will
|
||||
/// contain string paths.
|
||||
/// </summary>
|
||||
internal const uint SHCNF_PATH = 0x0001;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- Copyright (c) Microsoft Corporation and Contributors. -->
|
||||
<!-- Copyright (c) Microsoft Corporation and Contributors. -->
|
||||
<!-- Licensed under the MIT License. -->
|
||||
|
||||
<winuiex:WindowEx
|
||||
@@ -18,7 +18,7 @@
|
||||
<MicaBackdrop />
|
||||
</Window.SystemBackdrop>
|
||||
|
||||
<Grid KeyboardAcceleratorPlacementMode="Hidden">
|
||||
<Grid Name="MainGrid" KeyboardAcceleratorPlacementMode="Hidden">
|
||||
<Grid.KeyboardAccelerators>
|
||||
<KeyboardAccelerator Key="Left" Invoked="PreviousNavigationInvoked" />
|
||||
<KeyboardAccelerator Key="Up" Invoked="PreviousNavigationInvoked" />
|
||||
@@ -34,20 +34,46 @@
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<views:TitleBar
|
||||
x:Name="TitleBarControl"
|
||||
Grid.Row="0"
|
||||
FileIndex="{x:Bind ViewModel.CurrentIndex, Mode=OneWay}"
|
||||
FileIndex="{x:Bind ViewModel.DisplayIndex, 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}" />
|
||||
NumberOfFiles="{x:Bind ViewModel.DisplayItemCount, Mode=OneWay}" />
|
||||
|
||||
<fp:FilePreview
|
||||
Grid.Row="1"
|
||||
Item="{x:Bind ViewModel.CurrentItem, Mode=OneWay}"
|
||||
NumberOfFiles="{x:Bind ViewModel.DisplayItemCount, Mode=OneWay}"
|
||||
PreviewSizeChanged="FilePreviewer_PreviewSizeChanged"
|
||||
ScalingFactor="{x:Bind ViewModel.ScalingFactor, Mode=OneWay}" />
|
||||
|
||||
<InfoBar
|
||||
x:Name="ErrorInfoBar"
|
||||
Title="Error"
|
||||
Grid.Row="1"
|
||||
Grid.RowSpan="2"
|
||||
Margin="4,0,4,6"
|
||||
VerticalAlignment="Bottom"
|
||||
IsOpen="{x:Bind ViewModel.IsErrorVisible, Mode=TwoWay}"
|
||||
Message="{x:Bind ViewModel.ErrorMessage, Mode=OneWay}"
|
||||
Severity="Error" />
|
||||
|
||||
<ContentDialog
|
||||
x:Name="DeleteConfirmationDialog"
|
||||
x:Uid="DeleteConfirmationDialog"
|
||||
DefaultButton="Close">
|
||||
<StackPanel
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Spacing="12">
|
||||
<TextBlock x:Uid="DeleteConfirmationDialog_Message" TextWrapping="Wrap" />
|
||||
<CheckBox x:Name="DeleteDontWarnCheckbox" x:Uid="DeleteConfirmationDialog_DontWarnCheckbox" />
|
||||
</StackPanel>
|
||||
</ContentDialog>
|
||||
</Grid>
|
||||
</winuiex:WindowEx>
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Peek.Common.Constants;
|
||||
using Peek.Common.Extensions;
|
||||
@@ -30,6 +32,12 @@ namespace Peek.UI
|
||||
|
||||
private readonly ThemeListener? themeListener;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the delete confirmation dialog is currently open. Used to ensure only one
|
||||
/// dialog is open at a time.
|
||||
/// </summary>
|
||||
private bool _isDeleteInProgress;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -56,6 +64,54 @@ namespace Peek.UI
|
||||
AppWindow.Closing += AppWindow_Closing;
|
||||
}
|
||||
|
||||
private async void Content_KeyUp(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
if (e.Key == Windows.System.VirtualKey.Delete)
|
||||
{
|
||||
e.Handled = true;
|
||||
await DeleteItem();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteItem()
|
||||
{
|
||||
if (ViewModel.CurrentItem == null || _isDeleteInProgress)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_isDeleteInProgress = true;
|
||||
|
||||
if (Application.Current.GetService<IUserSettings>().ConfirmFileDelete)
|
||||
{
|
||||
if (await ShowDeleteConfirmationDialogAsync() == ContentDialogResult.Primary)
|
||||
{
|
||||
// Delete after asking for confirmation. Persist the "Don't warn again" choice if set.
|
||||
ViewModel.DeleteItem(DeleteDontWarnCheckbox.IsChecked, this.GetWindowHandle());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Delete without confirmation.
|
||||
ViewModel.DeleteItem(true, this.GetWindowHandle());
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isDeleteInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ContentDialogResult> ShowDeleteConfirmationDialogAsync()
|
||||
{
|
||||
DeleteDontWarnCheckbox.IsChecked = false;
|
||||
DeleteConfirmationDialog.XamlRoot = Content.XamlRoot;
|
||||
|
||||
return await DeleteConfirmationDialog.ShowAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggling the window visibility and querying files when necessary.
|
||||
/// </summary>
|
||||
@@ -68,6 +124,11 @@ namespace Peek.UI
|
||||
return;
|
||||
}
|
||||
|
||||
if (DeleteConfirmationDialog.Visibility == Visibility.Visible)
|
||||
{
|
||||
DeleteConfirmationDialog.Hide();
|
||||
}
|
||||
|
||||
if (AppWindow.IsVisible)
|
||||
{
|
||||
if (IsNewSingleSelectedItem(foregroundWindowHandle))
|
||||
@@ -127,6 +188,7 @@ namespace Peek.UI
|
||||
|
||||
ViewModel.Initialize(foregroundWindowHandle);
|
||||
ViewModel.ScalingFactor = this.GetMonitorScale();
|
||||
this.Content.KeyUp += Content_KeyUp;
|
||||
|
||||
bootTime.Stop();
|
||||
|
||||
@@ -140,6 +202,8 @@ namespace Peek.UI
|
||||
|
||||
ViewModel.Uninitialize();
|
||||
ViewModel.ScalingFactor = 1;
|
||||
|
||||
this.Content.KeyUp -= Content_KeyUp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
x:Name="AppTitle_FileName"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind Item.Name, Mode=OneWay}"
|
||||
Text="{x:Bind FileName, Mode=OneWay}"
|
||||
TextWrapping="NoWrap" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace Peek.UI.Views
|
||||
nameof(NumberOfFiles),
|
||||
typeof(int),
|
||||
typeof(TitleBar),
|
||||
new PropertyMetadata(null, null));
|
||||
new PropertyMetadata(null, (d, e) => ((TitleBar)d).OnNumberOfFilesPropertyChanged()));
|
||||
|
||||
[ObservableProperty]
|
||||
private string openWithAppText = ResourceLoaderInstance.ResourceLoader.GetString("LaunchAppButton_OpenWith_Text");
|
||||
@@ -66,6 +66,9 @@ namespace Peek.UI.Views
|
||||
[ObservableProperty]
|
||||
private string? fileCountText;
|
||||
|
||||
[ObservableProperty]
|
||||
private string fileName = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string defaultAppName = string.Empty;
|
||||
|
||||
@@ -242,28 +245,40 @@ namespace Peek.UI.Views
|
||||
|
||||
private void OnFilePropertyChanged()
|
||||
{
|
||||
if (Item == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateFileCountText();
|
||||
UpdateFilename();
|
||||
UpdateDefaultAppToLaunch();
|
||||
}
|
||||
|
||||
private void UpdateFilename()
|
||||
{
|
||||
FileName = Item?.Name ?? string.Empty;
|
||||
}
|
||||
|
||||
private void OnFileIndexPropertyChanged()
|
||||
{
|
||||
UpdateFileCountText();
|
||||
}
|
||||
|
||||
private void OnNumberOfFilesPropertyChanged()
|
||||
{
|
||||
UpdateFileCountText();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Respond to a change in the current file being previewed or the number of files available.
|
||||
/// </summary>
|
||||
private void UpdateFileCountText()
|
||||
{
|
||||
// Update file count
|
||||
if (NumberOfFiles > 1)
|
||||
if (NumberOfFiles >= 1)
|
||||
{
|
||||
string fileCountTextFormat = ResourceLoaderInstance.ResourceLoader.GetString("AppTitle_FileCounts_Text");
|
||||
FileCountText = string.Format(CultureInfo.InvariantCulture, fileCountTextFormat, FileIndex + 1, NumberOfFiles);
|
||||
}
|
||||
else
|
||||
{
|
||||
FileCountText = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateDefaultAppToLaunch()
|
||||
|
||||
@@ -7,5 +7,7 @@ namespace Peek.UI
|
||||
public interface IUserSettings
|
||||
{
|
||||
public bool CloseAfterLosingFocus { get; }
|
||||
|
||||
public bool ConfirmFileDelete { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,58 @@ namespace Peek.UI
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0052:Remove unread private members", Justification = "Defined in helper called in constructor.")]
|
||||
private readonly IFileSystemWatcher _watcher;
|
||||
|
||||
/// <summary>
|
||||
/// The current settings. Initially set to defaults.
|
||||
/// </summary>
|
||||
private PeekSettings _settings = new();
|
||||
|
||||
private PeekSettings Settings
|
||||
{
|
||||
get => _settings;
|
||||
set
|
||||
{
|
||||
lock (_settingsLock)
|
||||
{
|
||||
_settings = value;
|
||||
CloseAfterLosingFocus = _settings.Properties.CloseAfterLosingFocus.Value;
|
||||
ConfirmFileDelete = _settings.Properties.ConfirmFileDelete.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether Peek closes automatically when the window loses focus.
|
||||
/// </summary>
|
||||
public bool CloseAfterLosingFocus { get; private set; }
|
||||
|
||||
private bool _confirmFileDelete;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the user is prompted before a file is recycled.
|
||||
/// </summary>
|
||||
/// <remarks>The user will always be prompted when the file cannot be sent to the Recycle
|
||||
/// Bin and would instead be permanently deleted.</remarks>
|
||||
public bool ConfirmFileDelete
|
||||
{
|
||||
get => _confirmFileDelete;
|
||||
set
|
||||
{
|
||||
if (_confirmFileDelete != value)
|
||||
{
|
||||
_confirmFileDelete = value;
|
||||
|
||||
// We write directly to the settings file. The Settings UI will pick detect
|
||||
// this change via its file watcher and update accordingly. This is the only
|
||||
// setting that is modified by Peek itself.
|
||||
lock (_settingsLock)
|
||||
{
|
||||
_settings.Properties.ConfirmFileDelete.Value = _confirmFileDelete;
|
||||
_settingsUtils.SaveSettings(_settings.ToJsonString(), PeekModuleName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public UserSettings()
|
||||
{
|
||||
_settingsUtils = new SettingsUtils();
|
||||
@@ -37,26 +84,13 @@ namespace Peek.UI
|
||||
_watcher = Helper.GetFileWatcher(PeekModuleName, SettingsUtils.DefaultFileName, LoadSettingsFromJson);
|
||||
}
|
||||
|
||||
private void ApplySettings(PeekSettings settings)
|
||||
{
|
||||
lock (_settingsLock)
|
||||
{
|
||||
CloseAfterLosingFocus = settings.Properties.CloseAfterLosingFocus.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyDefaultSettings()
|
||||
{
|
||||
ApplySettings(new PeekSettings());
|
||||
}
|
||||
|
||||
private void LoadSettingsFromJson()
|
||||
{
|
||||
for (int attempt = 1; attempt <= MaxAttempts; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
ApplySettings(_settingsUtils.GetSettingsOrDefault<PeekSettings>(PeekModuleName));
|
||||
Settings = _settingsUtils.GetSettingsOrDefault<PeekSettings>(PeekModuleName);
|
||||
return;
|
||||
}
|
||||
catch (System.IO.IOException ex)
|
||||
@@ -65,8 +99,7 @@ namespace Peek.UI
|
||||
if (attempt == MaxAttempts)
|
||||
{
|
||||
Logger.LogError($"Failed to load Peek settings after {MaxAttempts} attempts. Continuing with default settings.");
|
||||
ApplyDefaultSettings();
|
||||
return;
|
||||
break;
|
||||
}
|
||||
|
||||
// Exponential back-off then retry.
|
||||
@@ -76,10 +109,10 @@ namespace Peek.UI
|
||||
{
|
||||
// Anything other than an IO exception is an immediate failure.
|
||||
Logger.LogError($"Peek settings load failed, continuing with defaults: {ex.Message}", ex);
|
||||
ApplyDefaultSettings();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Settings = new PeekSettings();
|
||||
}
|
||||
|
||||
private static int CalculateRetryDelay(int attempt)
|
||||
|
||||
@@ -329,4 +329,47 @@
|
||||
<data name="ContextMenu_ToggleMinimap" xml:space="preserve">
|
||||
<value>Toggle minimap</value>
|
||||
</data>
|
||||
<data name="NoMoreFiles" xml:space="preserve">
|
||||
<value>No more files to preview.</value>
|
||||
<comment>The message to show when there are no files remaining to preview.</comment>
|
||||
</data>
|
||||
<data name="DeleteFileError_NotFound" xml:space="preserve">
|
||||
<value>The file cannot be found. Please check if the file has been moved, renamed, or deleted.</value>
|
||||
<comment>Displayed if the file or path was not found</comment>
|
||||
</data>
|
||||
<data name="DeleteFileError_AccessDenied" xml:space="preserve">
|
||||
<value>Access is denied. Please ensure you have permission to delete the file.</value>
|
||||
<comment>Displayed if access to the file was denied when trying to delete it</comment>
|
||||
</data>
|
||||
<data name="DeleteFileError_Generic" xml:space="preserve">
|
||||
<value>An error occurred while deleting the file. Please try again later.</value>
|
||||
<comment>Displayed if the file could not be deleted and no other error code matched</comment>
|
||||
</data>
|
||||
<data name="DeleteFileError_FileInUse" xml:space="preserve">
|
||||
<value>The file is currently in use by another program. Please close any programs that might be using the file, then try again.</value>
|
||||
<comment>Displayed if the file could not be deleted because it is fully or partially locked by another process</comment>
|
||||
</data>
|
||||
<data name="DeleteFileError_WriteProtected" xml:space="preserve">
|
||||
<value>The storage medium is write-protected. If possible, remove the write protection then try again.</value>
|
||||
<comment>Displayed if the file could not be deleted because it exists on non-writable media</comment>
|
||||
</data>
|
||||
<data name="DeleteFileError_Prefix" xml:space="preserve">
|
||||
<value>Cannot delete '{0}'.</value>
|
||||
<comment>The prefix added to all file delete failure messages. {0} is replaced with the name of the file</comment>
|
||||
</data>
|
||||
<data name="DeleteConfirmationDialog.Title" xml:space="preserve">
|
||||
<value>Delete file?</value>
|
||||
</data>
|
||||
<data name="DeleteConfirmationDialog.PrimaryButtonText" xml:space="preserve">
|
||||
<value>Delete</value>
|
||||
</data>
|
||||
<data name="DeleteConfirmationDialog.CloseButtonText" xml:space="preserve">
|
||||
<value>Cancel</value>
|
||||
</data>
|
||||
<data name="DeleteConfirmationDialog_Message.Text" xml:space="preserve">
|
||||
<value>Are you sure you want to delete this file?</value>
|
||||
</data>
|
||||
<data name="DeleteConfirmationDialog_DontWarnCheckbox.Content" xml:space="preserve">
|
||||
<value>Don't show this warning again</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -10,21 +10,23 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
public interface ISettingsUtils
|
||||
{
|
||||
T GetSettings<T>(string powertoy = "", string fileName = "settings.json")
|
||||
public const string DefaultFileName = "settings.json";
|
||||
|
||||
T GetSettings<T>(string powertoy = "", string fileName = DefaultFileName)
|
||||
where T : ISettingsConfig, new();
|
||||
|
||||
T GetSettingsOrDefault<T>(string powertoy = "", string fileName = "settings.json")
|
||||
T GetSettingsOrDefault<T>(string powertoy = "", string fileName = DefaultFileName)
|
||||
where T : ISettingsConfig, new();
|
||||
|
||||
void SaveSettings(string jsonSettings, string powertoy = "", string fileName = "settings.json");
|
||||
void SaveSettings(string jsonSettings, string powertoy = "", string fileName = DefaultFileName);
|
||||
|
||||
bool SettingsExists(string powertoy = "", string fileName = "settings.json");
|
||||
bool SettingsExists(string powertoy = "", string fileName = DefaultFileName);
|
||||
|
||||
void DeleteSettings(string powertoy = "");
|
||||
|
||||
string GetSettingsFilePath(string powertoy = "", string fileName = "settings.json");
|
||||
string GetSettingsFilePath(string powertoy = "", string fileName = DefaultFileName);
|
||||
|
||||
T GetSettingsOrDefault<T, T2>(string powertoy = "", string fileName = "settings.json", Func<object, object> settingsUpgrader = null)
|
||||
T GetSettingsOrDefault<T, T2>(string powertoy = "", string fileName = DefaultFileName, Func<object, object> settingsUpgrader = null)
|
||||
where T : ISettingsConfig, new()
|
||||
where T2 : ISettingsConfig, new();
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
ActivationShortcut = DefaultActivationShortcut;
|
||||
AlwaysRunNotElevated = new BoolProperty(true);
|
||||
CloseAfterLosingFocus = new BoolProperty(false);
|
||||
ConfirmFileDelete = new BoolProperty(true);
|
||||
}
|
||||
|
||||
public HotkeySettings ActivationShortcut { get; set; }
|
||||
@@ -26,6 +27,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
public BoolProperty CloseAfterLosingFocus { get; set; }
|
||||
|
||||
public BoolProperty ConfirmFileDelete { get; set; }
|
||||
|
||||
public override string ToString() => JsonSerializer.Serialize(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,9 @@
|
||||
<tkcontrols:SettingsCard x:Uid="Peek_CloseAfterLosingFocus" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.CloseAfterLosingFocus, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="Peek_ConfirmFileDelete" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.ConfirmFileDelete, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
</controls:SettingsGroup>
|
||||
|
||||
<controls:SettingsGroup x:Uid="Peek_Preview_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
||||
|
||||
@@ -16,7 +16,11 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
public PeekPage()
|
||||
{
|
||||
var settingsUtils = new SettingsUtils();
|
||||
ViewModel = new PeekViewModel(settingsUtils, SettingsRepository<GeneralSettings>.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage);
|
||||
ViewModel = new PeekViewModel(
|
||||
settingsUtils,
|
||||
SettingsRepository<GeneralSettings>.GetInstance(settingsUtils),
|
||||
ShellPage.SendDefaultIPCMessage,
|
||||
DispatcherQueue);
|
||||
DataContext = ViewModel;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
@@ -3050,6 +3050,12 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<value>Automatically close the Peek window after it loses focus</value>
|
||||
<comment>Peek is a product name, do not loc</comment>
|
||||
</data>
|
||||
<data name="Peek_ConfirmFileDelete.Header" xml:space="preserve">
|
||||
<value>Ask for confirmation before deleting files</value>
|
||||
</data>
|
||||
<data name="Peek_ConfirmFileDelete.Description" xml:space="preserve">
|
||||
<value>When enabled, you will be prompted to confirm before moving files to the Recycle Bin.</value>
|
||||
</data>
|
||||
<data name="FancyZones_DisableRoundCornersOnWindowSnap.Content" xml:space="preserve">
|
||||
<value>Disable round corners when window is snapped</value>
|
||||
</data>
|
||||
|
||||
@@ -4,63 +4,105 @@
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using System.Text.Json;
|
||||
|
||||
using global::PowerToys.GPOWrapper;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
|
||||
using Microsoft.PowerToys.Settings.UI.SerializationContext;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Settings.UI.Library;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
public partial class PeekViewModel : Observable
|
||||
public class PeekViewModel : Observable, IDisposable
|
||||
{
|
||||
private bool _isEnabled;
|
||||
|
||||
private bool _settingsUpdating;
|
||||
|
||||
private GeneralSettings GeneralSettingsConfig { get; set; }
|
||||
|
||||
private readonly DispatcherQueue _dispatcherQueue;
|
||||
|
||||
private readonly ISettingsUtils _settingsUtils;
|
||||
private readonly PeekSettings _peekSettings;
|
||||
private readonly PeekPreviewSettings _peekPreviewSettings;
|
||||
private PeekSettings _peekSettings;
|
||||
|
||||
private GpoRuleConfigured _enabledGpoRuleConfiguration;
|
||||
private bool _enabledStateIsGPOConfigured;
|
||||
|
||||
private Func<string, int> SendConfigMSG { get; }
|
||||
|
||||
public PeekViewModel(ISettingsUtils settingsUtils, ISettingsRepository<GeneralSettings> settingsRepository, Func<string, int> ipcMSGCallBackFunc)
|
||||
private IFileSystemWatcher _watcher;
|
||||
|
||||
public PeekViewModel(
|
||||
ISettingsUtils settingsUtils,
|
||||
ISettingsRepository<GeneralSettings> settingsRepository,
|
||||
Func<string, int> ipcMSGCallBackFunc,
|
||||
DispatcherQueue dispatcherQueue)
|
||||
{
|
||||
// To obtain the general settings configurations of PowerToys Settings.
|
||||
ArgumentNullException.ThrowIfNull(settingsRepository);
|
||||
|
||||
GeneralSettingsConfig = settingsRepository.SettingsConfig;
|
||||
|
||||
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
|
||||
if (_settingsUtils.SettingsExists(PeekSettings.ModuleName))
|
||||
{
|
||||
_peekSettings = _settingsUtils.GetSettingsOrDefault<PeekSettings>(PeekSettings.ModuleName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_peekSettings = new PeekSettings();
|
||||
}
|
||||
_dispatcherQueue = dispatcherQueue ?? throw new ArgumentNullException(nameof(dispatcherQueue));
|
||||
|
||||
if (_settingsUtils.SettingsExists(PeekSettings.ModuleName, PeekPreviewSettings.FileName))
|
||||
{
|
||||
_peekPreviewSettings = _settingsUtils.GetSettingsOrDefault<PeekPreviewSettings>(PeekSettings.ModuleName, PeekPreviewSettings.FileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_peekPreviewSettings = new PeekPreviewSettings();
|
||||
}
|
||||
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
|
||||
|
||||
// Load the application-specific settings, including preview items.
|
||||
_peekSettings = _settingsUtils.GetSettingsOrDefault<PeekSettings>(PeekSettings.ModuleName);
|
||||
_peekPreviewSettings = _settingsUtils.GetSettingsOrDefault<PeekPreviewSettings>(PeekSettings.ModuleName, PeekPreviewSettings.FileName);
|
||||
SetupSettingsFileWatcher();
|
||||
|
||||
InitializeEnabledValue();
|
||||
|
||||
SendConfigMSG = ipcMSGCallBackFunc;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set up the file watcher for the settings file. Used to respond to updates to the
|
||||
/// ConfirmFileDelete setting by the user within the Peek application itself.
|
||||
/// </summary>
|
||||
private void SetupSettingsFileWatcher()
|
||||
{
|
||||
string settingsPath = _settingsUtils.GetSettingsFilePath(PeekSettings.ModuleName);
|
||||
|
||||
_watcher = Helper.GetFileWatcher(PeekSettings.ModuleName, SettingsUtils.DefaultFileName, () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
_settingsUpdating = true;
|
||||
var newSettings = _settingsUtils.GetSettings<PeekSettings>(PeekSettings.ModuleName);
|
||||
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
ConfirmFileDelete = newSettings.Properties.ConfirmFileDelete.Value;
|
||||
_peekSettings = newSettings;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Only clear the flag once the UI update is complete.
|
||||
_settingsUpdating = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to load Peek settings: {ex.Message}", ex);
|
||||
_settingsUpdating = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void InitializeEnabledValue()
|
||||
{
|
||||
_enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredPeekEnabledValue();
|
||||
@@ -147,6 +189,20 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public bool ConfirmFileDelete
|
||||
{
|
||||
get => _peekSettings.Properties.ConfirmFileDelete.Value;
|
||||
set
|
||||
{
|
||||
if (_peekSettings.Properties.ConfirmFileDelete.Value != value)
|
||||
{
|
||||
_peekSettings.Properties.ConfirmFileDelete.Value = value;
|
||||
OnPropertyChanged(nameof(ConfirmFileDelete));
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool SourceCodeWrapText
|
||||
{
|
||||
get => _peekPreviewSettings.SourceCodeWrapText.Value;
|
||||
@@ -219,7 +275,14 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
private void NotifySettingsChanged()
|
||||
{
|
||||
// Using InvariantCulture as this is an IPC message
|
||||
// Do not send IPC message if the settings file has been updated by Peek itself.
|
||||
if (_settingsUpdating)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// This message will be intercepted by the runner, which passes the serialized JSON to
|
||||
// Peek.set_config() in the C++ Peek project, which then saves it to file.
|
||||
SendConfigMSG(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
@@ -238,5 +301,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
InitializeEnabledValue();
|
||||
OnPropertyChanged(nameof(IsEnabled));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_watcher?.Dispose();
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user