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
|
flyouts
|
||||||
FMask
|
FMask
|
||||||
FOF
|
FOF
|
||||||
|
WANTNUKEWARNING
|
||||||
FOFX
|
FOFX
|
||||||
FOLDERID
|
FOLDERID
|
||||||
folderpath
|
folderpath
|
||||||
@@ -1418,6 +1419,7 @@ SHCNE
|
|||||||
SHCNF
|
SHCNF
|
||||||
SHCONTF
|
SHCONTF
|
||||||
Shcore
|
Shcore
|
||||||
|
shellapi
|
||||||
SHELLDETAILS
|
SHELLDETAILS
|
||||||
SHELLDLL
|
SHELLDLL
|
||||||
shellex
|
shellex
|
||||||
@@ -1812,6 +1814,7 @@ windowssearch
|
|||||||
windowssettings
|
windowssettings
|
||||||
WINDOWSTYLES
|
WINDOWSTYLES
|
||||||
WINDOWSTYLESICON
|
WINDOWSTYLESICON
|
||||||
|
winerror
|
||||||
WINEVENT
|
WINEVENT
|
||||||
winget
|
winget
|
||||||
wingetcreate
|
wingetcreate
|
||||||
|
|||||||
@@ -98,4 +98,4 @@
|
|||||||
<PackageVersion Include="Microsoft.VariantAssignment.Client" Version="2.4.17140001" />
|
<PackageVersion Include="Microsoft.VariantAssignment.Client" Version="2.4.17140001" />
|
||||||
<PackageVersion Include="Microsoft.VariantAssignment.Contract" Version="3.0.16990001" />
|
<PackageVersion Include="Microsoft.VariantAssignment.Contract" Version="3.0.16990001" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -102,6 +102,16 @@
|
|||||||
LoadingState="{x:Bind UnsupportedFilePreviewer.State, Mode=OneWay}"
|
LoadingState="{x:Bind UnsupportedFilePreviewer.State, Mode=OneWay}"
|
||||||
Source="{x:Bind UnsupportedFilePreviewer.Preview, Mode=OneWay}"
|
Source="{x:Bind UnsupportedFilePreviewer.Preview, Mode=OneWay}"
|
||||||
Visibility="{x:Bind IsUnsupportedPreviewVisible(UnsupportedFilePreviewer, Previewer.State), 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>
|
</Grid>
|
||||||
<UserControl.KeyboardAccelerators>
|
<UserControl.KeyboardAccelerators>
|
||||||
<KeyboardAccelerator
|
<KeyboardAccelerator
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ namespace Peek.FilePreviewer
|
|||||||
typeof(FilePreview),
|
typeof(FilePreview),
|
||||||
new PropertyMetadata(false, async (d, e) => await ((FilePreview)d).OnScalingFactorPropertyChanged()));
|
new PropertyMetadata(false, async (d, e) => await ((FilePreview)d).OnScalingFactorPropertyChanged()));
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private int numberOfFiles;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
[NotifyPropertyChangedFor(nameof(ImagePreviewer))]
|
[NotifyPropertyChangedFor(nameof(ImagePreviewer))]
|
||||||
[NotifyPropertyChangedFor(nameof(VideoPreviewer))]
|
[NotifyPropertyChangedFor(nameof(VideoPreviewer))]
|
||||||
@@ -62,6 +65,9 @@ namespace Peek.FilePreviewer
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string infoTooltip = ResourceLoaderInstance.ResourceLoader.GetString("PreviewTooltip_Blank");
|
private string infoTooltip = ResourceLoaderInstance.ResourceLoader.GetString("PreviewTooltip_Blank");
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string noMoreFilesText = ResourceLoaderInstance.ResourceLoader.GetString("NoMoreFiles");
|
||||||
|
|
||||||
private CancellationTokenSource _cancellationTokenSource = new();
|
private CancellationTokenSource _cancellationTokenSource = new();
|
||||||
|
|
||||||
public FilePreview()
|
public FilePreview()
|
||||||
@@ -158,6 +164,8 @@ namespace Peek.FilePreviewer
|
|||||||
// Clear up any unmanaged resources before creating a new previewer instance.
|
// Clear up any unmanaged resources before creating a new previewer instance.
|
||||||
(Previewer as IDisposable)?.Dispose();
|
(Previewer as IDisposable)?.Dispose();
|
||||||
|
|
||||||
|
NoMoreFiles.Visibility = NumberOfFiles == 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
|
||||||
if (Item == null)
|
if (Item == null)
|
||||||
{
|
{
|
||||||
Previewer = 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.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
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 CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using ManagedCommon;
|
using ManagedCommon;
|
||||||
|
using Microsoft.UI.Dispatching;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
|
using Peek.Common.Extensions;
|
||||||
using Peek.Common.Helpers;
|
using Peek.Common.Helpers;
|
||||||
using Peek.Common.Models;
|
using Peek.Common.Models;
|
||||||
|
using Peek.UI.Helpers;
|
||||||
using Peek.UI.Models;
|
using Peek.UI.Models;
|
||||||
using Windows.Win32.Foundation;
|
using Windows.Win32.Foundation;
|
||||||
|
using static Peek.UI.Native.NativeMethods;
|
||||||
|
|
||||||
namespace Peek.UI
|
namespace Peek.UI
|
||||||
{
|
{
|
||||||
public partial class MainWindowViewModel : ObservableObject
|
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;
|
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;
|
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]
|
[ObservableProperty]
|
||||||
private IFileSystemItem? _currentItem;
|
private IFileSystemItem? _currentItem;
|
||||||
|
|
||||||
@@ -37,11 +73,49 @@ namespace Peek.UI
|
|||||||
private string _windowTitle;
|
private string _windowTitle;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(DisplayItemCount))]
|
||||||
private NeighboringItems? _items;
|
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]
|
[ObservableProperty]
|
||||||
private double _scalingFactor = 1.0;
|
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; }
|
public NeighboringItemsQuery NeighboringItemsQuery { get; }
|
||||||
|
|
||||||
private DispatcherTimer NavigationThrottleTimer { get; set; } = new();
|
private DispatcherTimer NavigationThrottleTimer { get; set; } = new();
|
||||||
@@ -63,50 +137,215 @@ namespace Peek.UI
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
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 != null && Items.Count > 0) ? Items[0] : null;
|
||||||
{
|
|
||||||
CurrentItem = Items[0];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Uninitialize()
|
public void Uninitialize()
|
||||||
{
|
{
|
||||||
CurrentIndex = 0;
|
_currentIndex = DisplayIndex = 0;
|
||||||
CurrentItem = null;
|
CurrentItem = null;
|
||||||
|
_deletedItemIndexes.Clear();
|
||||||
Items = null;
|
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)
|
if (NavigationThrottleTimer.IsEnabled)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigationThrottleTimer.Start();
|
if (Items == null || Items.Count == _deletedItemIndexes.Count)
|
||||||
|
{
|
||||||
|
_currentIndex = DisplayIndex = 0;
|
||||||
|
CurrentItem = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var itemCount = Items?.Count ?? 1;
|
_navigationDirection = direction;
|
||||||
CurrentIndex = MathHelper.Modulo(CurrentIndex - 1, itemCount);
|
|
||||||
CurrentItem = Items?.ElementAtOrDefault(CurrentIndex);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigationThrottleTimer.Start();
|
bool skipConfirmation = skipConfirmationChecked ?? false;
|
||||||
|
bool shouldShowConfirmation = !skipConfirmation;
|
||||||
|
Application.Current.GetService<IUserSettings>().ConfirmFileDelete = shouldShowConfirmation;
|
||||||
|
|
||||||
var itemCount = Items?.Count ?? 1;
|
var item = CurrentItem;
|
||||||
CurrentIndex = MathHelper.Modulo(CurrentIndex + 1, itemCount);
|
|
||||||
CurrentItem = Items?.ElementAtOrDefault(CurrentIndex);
|
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)
|
private void NavigationThrottleTimer_Tick(object? sender, object e)
|
||||||
|
|||||||
@@ -27,14 +27,8 @@ namespace Peek.UI.Models
|
|||||||
Items = new IFileSystemItem[Count];
|
Items = new IFileSystemItem[Count];
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerator<IFileSystemItem> GetEnumerator()
|
public IEnumerator<IFileSystemItem> GetEnumerator() => new NeighboringItemsEnumerator(this);
|
||||||
{
|
|
||||||
return new NeighboringItemsEnumerator(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||||
{
|
|
||||||
return GetEnumerator();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
using Peek.Common.Models;
|
using Peek.Common.Models;
|
||||||
|
|
||||||
namespace Peek.UI.Native
|
namespace Peek.UI.Native
|
||||||
@@ -51,5 +51,85 @@ namespace Peek.UI.Native
|
|||||||
|
|
||||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||||
internal static extern int GetClassName(IntPtr hWnd, StringBuilder buf, int nMaxCount);
|
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. -->
|
<!-- Licensed under the MIT License. -->
|
||||||
|
|
||||||
<winuiex:WindowEx
|
<winuiex:WindowEx
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<MicaBackdrop />
|
<MicaBackdrop />
|
||||||
</Window.SystemBackdrop>
|
</Window.SystemBackdrop>
|
||||||
|
|
||||||
<Grid KeyboardAcceleratorPlacementMode="Hidden">
|
<Grid Name="MainGrid" KeyboardAcceleratorPlacementMode="Hidden">
|
||||||
<Grid.KeyboardAccelerators>
|
<Grid.KeyboardAccelerators>
|
||||||
<KeyboardAccelerator Key="Left" Invoked="PreviousNavigationInvoked" />
|
<KeyboardAccelerator Key="Left" Invoked="PreviousNavigationInvoked" />
|
||||||
<KeyboardAccelerator Key="Up" Invoked="PreviousNavigationInvoked" />
|
<KeyboardAccelerator Key="Up" Invoked="PreviousNavigationInvoked" />
|
||||||
@@ -34,20 +34,46 @@
|
|||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<views:TitleBar
|
<views:TitleBar
|
||||||
x:Name="TitleBarControl"
|
x:Name="TitleBarControl"
|
||||||
Grid.Row="0"
|
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}"
|
IsMultiSelection="{x:Bind ViewModel.NeighboringItemsQuery.IsMultipleFilesActivation, Mode=OneWay}"
|
||||||
Item="{x:Bind ViewModel.CurrentItem, 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
|
<fp:FilePreview
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Item="{x:Bind ViewModel.CurrentItem, Mode=OneWay}"
|
Item="{x:Bind ViewModel.CurrentItem, Mode=OneWay}"
|
||||||
|
NumberOfFiles="{x:Bind ViewModel.DisplayItemCount, Mode=OneWay}"
|
||||||
PreviewSizeChanged="FilePreviewer_PreviewSizeChanged"
|
PreviewSizeChanged="FilePreviewer_PreviewSizeChanged"
|
||||||
ScalingFactor="{x:Bind ViewModel.ScalingFactor, Mode=OneWay}" />
|
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>
|
</Grid>
|
||||||
</winuiex:WindowEx>
|
</winuiex:WindowEx>
|
||||||
|
|||||||
@@ -3,12 +3,14 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using ManagedCommon;
|
using ManagedCommon;
|
||||||
using Microsoft.PowerToys.Telemetry;
|
using Microsoft.PowerToys.Telemetry;
|
||||||
using Microsoft.UI;
|
using Microsoft.UI;
|
||||||
using Microsoft.UI.Windowing;
|
using Microsoft.UI.Windowing;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Microsoft.UI.Xaml.Input;
|
using Microsoft.UI.Xaml.Input;
|
||||||
using Peek.Common.Constants;
|
using Peek.Common.Constants;
|
||||||
using Peek.Common.Extensions;
|
using Peek.Common.Extensions;
|
||||||
@@ -30,6 +32,12 @@ namespace Peek.UI
|
|||||||
|
|
||||||
private readonly ThemeListener? themeListener;
|
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()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
@@ -56,6 +64,54 @@ namespace Peek.UI
|
|||||||
AppWindow.Closing += AppWindow_Closing;
|
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>
|
/// <summary>
|
||||||
/// Toggling the window visibility and querying files when necessary.
|
/// Toggling the window visibility and querying files when necessary.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -68,6 +124,11 @@ namespace Peek.UI
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (DeleteConfirmationDialog.Visibility == Visibility.Visible)
|
||||||
|
{
|
||||||
|
DeleteConfirmationDialog.Hide();
|
||||||
|
}
|
||||||
|
|
||||||
if (AppWindow.IsVisible)
|
if (AppWindow.IsVisible)
|
||||||
{
|
{
|
||||||
if (IsNewSingleSelectedItem(foregroundWindowHandle))
|
if (IsNewSingleSelectedItem(foregroundWindowHandle))
|
||||||
@@ -127,6 +188,7 @@ namespace Peek.UI
|
|||||||
|
|
||||||
ViewModel.Initialize(foregroundWindowHandle);
|
ViewModel.Initialize(foregroundWindowHandle);
|
||||||
ViewModel.ScalingFactor = this.GetMonitorScale();
|
ViewModel.ScalingFactor = this.GetMonitorScale();
|
||||||
|
this.Content.KeyUp += Content_KeyUp;
|
||||||
|
|
||||||
bootTime.Stop();
|
bootTime.Stop();
|
||||||
|
|
||||||
@@ -140,6 +202,8 @@ namespace Peek.UI
|
|||||||
|
|
||||||
ViewModel.Uninitialize();
|
ViewModel.Uninitialize();
|
||||||
ViewModel.ScalingFactor = 1;
|
ViewModel.ScalingFactor = 1;
|
||||||
|
|
||||||
|
this.Content.KeyUp -= Content_KeyUp;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
x:Name="AppTitle_FileName"
|
x:Name="AppTitle_FileName"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Style="{StaticResource CaptionTextBlockStyle}"
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
Text="{x:Bind Item.Name, Mode=OneWay}"
|
Text="{x:Bind FileName, Mode=OneWay}"
|
||||||
TextWrapping="NoWrap" />
|
TextWrapping="NoWrap" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ namespace Peek.UI.Views
|
|||||||
nameof(NumberOfFiles),
|
nameof(NumberOfFiles),
|
||||||
typeof(int),
|
typeof(int),
|
||||||
typeof(TitleBar),
|
typeof(TitleBar),
|
||||||
new PropertyMetadata(null, null));
|
new PropertyMetadata(null, (d, e) => ((TitleBar)d).OnNumberOfFilesPropertyChanged()));
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string openWithAppText = ResourceLoaderInstance.ResourceLoader.GetString("LaunchAppButton_OpenWith_Text");
|
private string openWithAppText = ResourceLoaderInstance.ResourceLoader.GetString("LaunchAppButton_OpenWith_Text");
|
||||||
@@ -66,6 +66,9 @@ namespace Peek.UI.Views
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string? fileCountText;
|
private string? fileCountText;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string fileName = string.Empty;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string defaultAppName = string.Empty;
|
private string defaultAppName = string.Empty;
|
||||||
|
|
||||||
@@ -242,28 +245,40 @@ namespace Peek.UI.Views
|
|||||||
|
|
||||||
private void OnFilePropertyChanged()
|
private void OnFilePropertyChanged()
|
||||||
{
|
{
|
||||||
if (Item == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateFileCountText();
|
UpdateFileCountText();
|
||||||
|
UpdateFilename();
|
||||||
UpdateDefaultAppToLaunch();
|
UpdateDefaultAppToLaunch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateFilename()
|
||||||
|
{
|
||||||
|
FileName = Item?.Name ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
private void OnFileIndexPropertyChanged()
|
private void OnFileIndexPropertyChanged()
|
||||||
{
|
{
|
||||||
UpdateFileCountText();
|
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()
|
private void UpdateFileCountText()
|
||||||
{
|
{
|
||||||
// Update file count
|
if (NumberOfFiles >= 1)
|
||||||
if (NumberOfFiles > 1)
|
|
||||||
{
|
{
|
||||||
string fileCountTextFormat = ResourceLoaderInstance.ResourceLoader.GetString("AppTitle_FileCounts_Text");
|
string fileCountTextFormat = ResourceLoaderInstance.ResourceLoader.GetString("AppTitle_FileCounts_Text");
|
||||||
FileCountText = string.Format(CultureInfo.InvariantCulture, fileCountTextFormat, FileIndex + 1, NumberOfFiles);
|
FileCountText = string.Format(CultureInfo.InvariantCulture, fileCountTextFormat, FileIndex + 1, NumberOfFiles);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FileCountText = string.Empty;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateDefaultAppToLaunch()
|
private void UpdateDefaultAppToLaunch()
|
||||||
|
|||||||
@@ -7,5 +7,7 @@ namespace Peek.UI
|
|||||||
public interface IUserSettings
|
public interface IUserSettings
|
||||||
{
|
{
|
||||||
public bool CloseAfterLosingFocus { get; }
|
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.")]
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0052:Remove unread private members", Justification = "Defined in helper called in constructor.")]
|
||||||
private readonly IFileSystemWatcher _watcher;
|
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>
|
/// <summary>
|
||||||
/// Gets a value indicating whether Peek closes automatically when the window loses focus.
|
/// Gets a value indicating whether Peek closes automatically when the window loses focus.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool CloseAfterLosingFocus { get; private set; }
|
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()
|
public UserSettings()
|
||||||
{
|
{
|
||||||
_settingsUtils = new SettingsUtils();
|
_settingsUtils = new SettingsUtils();
|
||||||
@@ -37,26 +84,13 @@ namespace Peek.UI
|
|||||||
_watcher = Helper.GetFileWatcher(PeekModuleName, SettingsUtils.DefaultFileName, LoadSettingsFromJson);
|
_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()
|
private void LoadSettingsFromJson()
|
||||||
{
|
{
|
||||||
for (int attempt = 1; attempt <= MaxAttempts; attempt++)
|
for (int attempt = 1; attempt <= MaxAttempts; attempt++)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ApplySettings(_settingsUtils.GetSettingsOrDefault<PeekSettings>(PeekModuleName));
|
Settings = _settingsUtils.GetSettingsOrDefault<PeekSettings>(PeekModuleName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
catch (System.IO.IOException ex)
|
catch (System.IO.IOException ex)
|
||||||
@@ -65,8 +99,7 @@ namespace Peek.UI
|
|||||||
if (attempt == MaxAttempts)
|
if (attempt == MaxAttempts)
|
||||||
{
|
{
|
||||||
Logger.LogError($"Failed to load Peek settings after {MaxAttempts} attempts. Continuing with default settings.");
|
Logger.LogError($"Failed to load Peek settings after {MaxAttempts} attempts. Continuing with default settings.");
|
||||||
ApplyDefaultSettings();
|
break;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exponential back-off then retry.
|
// Exponential back-off then retry.
|
||||||
@@ -76,10 +109,10 @@ namespace Peek.UI
|
|||||||
{
|
{
|
||||||
// Anything other than an IO exception is an immediate failure.
|
// Anything other than an IO exception is an immediate failure.
|
||||||
Logger.LogError($"Peek settings load failed, continuing with defaults: {ex.Message}", ex);
|
Logger.LogError($"Peek settings load failed, continuing with defaults: {ex.Message}", ex);
|
||||||
ApplyDefaultSettings();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Settings = new PeekSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int CalculateRetryDelay(int attempt)
|
private static int CalculateRetryDelay(int attempt)
|
||||||
|
|||||||
@@ -329,4 +329,47 @@
|
|||||||
<data name="ContextMenu_ToggleMinimap" xml:space="preserve">
|
<data name="ContextMenu_ToggleMinimap" xml:space="preserve">
|
||||||
<value>Toggle minimap</value>
|
<value>Toggle minimap</value>
|
||||||
</data>
|
</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>
|
</root>
|
||||||
@@ -10,21 +10,23 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
{
|
{
|
||||||
public interface ISettingsUtils
|
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();
|
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();
|
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 = "");
|
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 T : ISettingsConfig, new()
|
||||||
where T2 : ISettingsConfig, new();
|
where T2 : ISettingsConfig, new();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
ActivationShortcut = DefaultActivationShortcut;
|
ActivationShortcut = DefaultActivationShortcut;
|
||||||
AlwaysRunNotElevated = new BoolProperty(true);
|
AlwaysRunNotElevated = new BoolProperty(true);
|
||||||
CloseAfterLosingFocus = new BoolProperty(false);
|
CloseAfterLosingFocus = new BoolProperty(false);
|
||||||
|
ConfirmFileDelete = new BoolProperty(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public HotkeySettings ActivationShortcut { get; set; }
|
public HotkeySettings ActivationShortcut { get; set; }
|
||||||
@@ -26,6 +27,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
|
|
||||||
public BoolProperty CloseAfterLosingFocus { get; set; }
|
public BoolProperty CloseAfterLosingFocus { get; set; }
|
||||||
|
|
||||||
|
public BoolProperty ConfirmFileDelete { get; set; }
|
||||||
|
|
||||||
public override string ToString() => JsonSerializer.Serialize(this);
|
public override string ToString() => JsonSerializer.Serialize(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,9 @@
|
|||||||
<tkcontrols:SettingsCard x:Uid="Peek_CloseAfterLosingFocus" HeaderIcon="{ui:FontIcon Glyph=}">
|
<tkcontrols:SettingsCard x:Uid="Peek_CloseAfterLosingFocus" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||||
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.CloseAfterLosingFocus, Mode=TwoWay}" />
|
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.CloseAfterLosingFocus, Mode=TwoWay}" />
|
||||||
</tkcontrols:SettingsCard>
|
</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>
|
||||||
|
|
||||||
<controls:SettingsGroup x:Uid="Peek_Preview_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
<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()
|
public PeekPage()
|
||||||
{
|
{
|
||||||
var settingsUtils = new SettingsUtils();
|
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;
|
DataContext = ViewModel;
|
||||||
InitializeComponent();
|
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>
|
<value>Automatically close the Peek window after it loses focus</value>
|
||||||
<comment>Peek is a product name, do not loc</comment>
|
<comment>Peek is a product name, do not loc</comment>
|
||||||
</data>
|
</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">
|
<data name="FancyZones_DisableRoundCornersOnWindowSnap.Content" xml:space="preserve">
|
||||||
<value>Disable round corners when window is snapped</value>
|
<value>Disable round corners when window is snapped</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -4,63 +4,105 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Abstractions;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
using global::PowerToys.GPOWrapper;
|
using global::PowerToys.GPOWrapper;
|
||||||
|
using ManagedCommon;
|
||||||
using Microsoft.PowerToys.Settings.UI.Library;
|
using Microsoft.PowerToys.Settings.UI.Library;
|
||||||
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||||
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
|
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
|
||||||
|
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
|
||||||
using Microsoft.PowerToys.Settings.UI.SerializationContext;
|
using Microsoft.PowerToys.Settings.UI.SerializationContext;
|
||||||
|
using Microsoft.UI.Dispatching;
|
||||||
using Settings.UI.Library;
|
using Settings.UI.Library;
|
||||||
|
|
||||||
namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||||
{
|
{
|
||||||
public partial class PeekViewModel : Observable
|
public class PeekViewModel : Observable, IDisposable
|
||||||
{
|
{
|
||||||
private bool _isEnabled;
|
private bool _isEnabled;
|
||||||
|
|
||||||
|
private bool _settingsUpdating;
|
||||||
|
|
||||||
private GeneralSettings GeneralSettingsConfig { get; set; }
|
private GeneralSettings GeneralSettingsConfig { get; set; }
|
||||||
|
|
||||||
|
private readonly DispatcherQueue _dispatcherQueue;
|
||||||
|
|
||||||
private readonly ISettingsUtils _settingsUtils;
|
private readonly ISettingsUtils _settingsUtils;
|
||||||
private readonly PeekSettings _peekSettings;
|
|
||||||
private readonly PeekPreviewSettings _peekPreviewSettings;
|
private readonly PeekPreviewSettings _peekPreviewSettings;
|
||||||
|
private PeekSettings _peekSettings;
|
||||||
|
|
||||||
private GpoRuleConfigured _enabledGpoRuleConfiguration;
|
private GpoRuleConfigured _enabledGpoRuleConfiguration;
|
||||||
private bool _enabledStateIsGPOConfigured;
|
private bool _enabledStateIsGPOConfigured;
|
||||||
|
|
||||||
private Func<string, int> SendConfigMSG { get; }
|
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.
|
// To obtain the general settings configurations of PowerToys Settings.
|
||||||
ArgumentNullException.ThrowIfNull(settingsRepository);
|
ArgumentNullException.ThrowIfNull(settingsRepository);
|
||||||
|
|
||||||
GeneralSettingsConfig = settingsRepository.SettingsConfig;
|
GeneralSettingsConfig = settingsRepository.SettingsConfig;
|
||||||
|
|
||||||
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
|
_dispatcherQueue = dispatcherQueue ?? throw new ArgumentNullException(nameof(dispatcherQueue));
|
||||||
if (_settingsUtils.SettingsExists(PeekSettings.ModuleName))
|
|
||||||
{
|
|
||||||
_peekSettings = _settingsUtils.GetSettingsOrDefault<PeekSettings>(PeekSettings.ModuleName);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_peekSettings = new PeekSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_settingsUtils.SettingsExists(PeekSettings.ModuleName, PeekPreviewSettings.FileName))
|
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
|
||||||
{
|
|
||||||
_peekPreviewSettings = _settingsUtils.GetSettingsOrDefault<PeekPreviewSettings>(PeekSettings.ModuleName, PeekPreviewSettings.FileName);
|
// Load the application-specific settings, including preview items.
|
||||||
}
|
_peekSettings = _settingsUtils.GetSettingsOrDefault<PeekSettings>(PeekSettings.ModuleName);
|
||||||
else
|
_peekPreviewSettings = _settingsUtils.GetSettingsOrDefault<PeekPreviewSettings>(PeekSettings.ModuleName, PeekPreviewSettings.FileName);
|
||||||
{
|
SetupSettingsFileWatcher();
|
||||||
_peekPreviewSettings = new PeekPreviewSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
InitializeEnabledValue();
|
InitializeEnabledValue();
|
||||||
|
|
||||||
SendConfigMSG = ipcMSGCallBackFunc;
|
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()
|
private void InitializeEnabledValue()
|
||||||
{
|
{
|
||||||
_enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredPeekEnabledValue();
|
_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
|
public bool SourceCodeWrapText
|
||||||
{
|
{
|
||||||
get => _peekPreviewSettings.SourceCodeWrapText.Value;
|
get => _peekPreviewSettings.SourceCodeWrapText.Value;
|
||||||
@@ -219,7 +275,14 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
|||||||
|
|
||||||
private void NotifySettingsChanged()
|
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(
|
SendConfigMSG(
|
||||||
string.Format(
|
string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
@@ -238,5 +301,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
|||||||
InitializeEnabledValue();
|
InitializeEnabledValue();
|
||||||
OnPropertyChanged(nameof(IsEnabled));
|
OnPropertyChanged(nameof(IsEnabled));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_watcher?.Dispose();
|
||||||
|
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user