Improve UI: Refine Recycle Bin icon display #1610

This commit is contained in:
ema
2025-05-10 06:51:14 +08:00
parent 1903687137
commit d51c8bb25e
7 changed files with 337 additions and 65 deletions

View File

@@ -15,6 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
using QuickLook.Common.Plugin;
using System.Windows;
using System.Windows.Controls;
@@ -28,26 +29,18 @@ public partial class CLSIDInfoPanel : UserControl
InitializeComponent();
}
public void DisplayInfo(string path)
public void DisplayInfo(string path, ContextObject context)
{
switch (path.ToUpper())
Content = path.ToUpper() switch
{
case CLSIDRegister.RecycleBin:
Content = new RecycleBinPanel();
break;
case CLSIDRegister.ThisPC:
Content = new ThisPCPanel();
break;
default:
Content = new TextBlock()
{
Text = $"Unsupported for {path}",
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
break;
}
CLSIDRegister.RecycleBin => new RecycleBinPanel(context),
CLSIDRegister.ThisPC => new ThisPCPanel(context),
_ => new TextBlock()
{
Text = $"Unsupported for {path}",
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
},
};
}
}

View File

@@ -49,7 +49,7 @@ public class Plugin : IViewer
{
context.PreferredSize = path switch
{
CLSIDRegister.RecycleBin => new Size { Width = 520, Height = 192 },
CLSIDRegister.RecycleBin => new Size { Width = 400, Height = 150 },
CLSIDRegister.ThisPC => new Size { Width = 900, Height = 800 },
_ => new Size { Width = 520, Height = 192 },
};
@@ -59,13 +59,11 @@ public class Plugin : IViewer
{
_path = path;
_ip = new CLSIDInfoPanel();
_ip.DisplayInfo(_path);
_ip.Tag = context;
_ip.DisplayInfo(path, context);
context.ViewerContent = _ip;
context.Title = $"{CLSIDRegister.GetName(path) ?? path}";
context.IsBusy = false;
context.IsBusy = true;
}
public void Cleanup()

View File

@@ -2,23 +2,54 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:QuickLook.Plugin.CLSIDViewer"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<Grid>
<TextBlock x:Name="EmptyRecycleBinText"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="Recycle Bin is empty" />
<Grid>
<Button x:Name="EmptyRecycleBinButton"
Grid.Column="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Click="OnEmptyRecycleBinClick"
Content="Empty Recycle Bin" />
<Grid Margin="0,-50,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image x:Name="image"
Grid.Row="0"
Grid.Column="0"
Width="48"
Height="48"
Opacity="1"
Stretch="Fill">
<Image.Style>
<Style TargetType="{x:Type Image}">
<Style.Triggers>
<DataTrigger Binding="{Binding Source, ElementName=image}" Value="{x:Null}">
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation BeginTime="0:0:0"
Storyboard.TargetProperty="Opacity"
From="0"
To="1"
Duration="0:0:0.05" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
<Grid Grid.Column="1" VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock x:Name="totalSizeAndCount"
Grid.Row="0"
Padding="3" />
<Button x:Name="emptyButton"
Grid.Row="1"
Margin="0,5,0,0"
Command="{Binding EmptyRecycleBinCommand}" />
</Grid>
</Grid>
</UserControl>

View File

@@ -15,46 +15,148 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
using Microsoft.Win32;
using QuickLook.Common.Commands;
using QuickLook.Common.Helpers;
using QuickLook.Common.Plugin;
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace QuickLook.Plugin.CLSIDViewer;
public partial class RecycleBinPanel : UserControl
public partial class RecycleBinPanel : UserControl, INotifyPropertyChanged
{
public RecycleBinPanel()
private ContextObject _context;
private RecycleBinHelper.RecycleBinInfo _info;
private ICommand _emptyRecycleBinCommand;
// ICommand must be used so that the button can be automatically disabled
public ICommand EmptyRecycleBinCommand =>
_emptyRecycleBinCommand ??= new AsyncRelayCommand(OnEmptyRecycleBinAsync);
public RecycleBinPanel(ContextObject context)
{
_context = context;
DataContext = this;
InitializeComponent();
Loaded += OnRecycleBinPanelLoaded;
emptyButton.Content = TranslationHelper.Get("RecycleBinButton",
domain: Assembly.GetExecutingAssembly().GetName().Name);
Loaded += OnLoaded;
}
private void OnRecycleBinPanelLoaded(object sender, RoutedEventArgs e)
protected virtual void OnLoaded(object sender, RoutedEventArgs e)
{
UpdateState();
}
private void OnEmptyRecycleBinClick(object sender, RoutedEventArgs e)
{
// TODO: Use async to avoid blocking the UI thread
if (RecycleBinHelper.EmptyRecycleBin())
_ = Task.Run(() =>
{
UpdateState();
_context.IsBusy = false;
});
}
private async Task OnEmptyRecycleBinAsync()
{
var result = MessageBox.Show(
string.Format(TranslationHelper.Get("ConfirmDeleteText", domain: Assembly.GetExecutingAssembly().GetName().Name), _info.TotalCount),
TranslationHelper.Get("RecycleBinButton", domain: Assembly.GetExecutingAssembly().GetName().Name),
MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (result == MessageBoxResult.Yes)
{
await Task.Run(() =>
{
if (RecycleBinHelper.EmptyRecycleBin())
{
UpdateState();
}
});
}
}
private void UpdateState()
{
bool hasTrash = RecycleBinHelper.HasTrash();
_info = RecycleBinHelper.GetRecycleBinInfo();
EmptyRecycleBinButton.Visibility = hasTrash ? Visibility.Visible : Visibility.Collapsed;
EmptyRecycleBinText.Visibility = hasTrash ? Visibility.Collapsed : Visibility.Visible;
Dispatcher.BeginInvoke(() =>
{
image.Source = _info.HasTrash ? _info.FullIcon : _info.EmptyIcon;
if (_info.HasTrash)
{
totalSizeAndCount.Text = string.Format(
TranslationHelper.Get("RecycleBinSizeText",
domain: Assembly.GetExecutingAssembly().GetName().Name),
_info.TotalSizeString,
_info.TotalCount
);
}
else
{
totalSizeAndCount.Text = TranslationHelper.Get("RecycleBinEmptyText",
domain: Assembly.GetExecutingAssembly().GetName().Name);
}
emptyButton.Visibility = _info.HasTrash ? Visibility.Visible : Visibility.Collapsed;
});
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
file static class RecycleBinHelper
internal static class RecycleBinHelper
{
public static ImageSource _emptyIcon = null;
public static ImageSource _fullIcon = null;
public sealed class RecycleBinInfo
{
/// <summary>
/// In bytes
/// </summary>
public ulong TotalSize { get; set; } = 0L;
public string TotalSizeString => FormatBytes(TotalSize);
public ulong TotalCount { get; set; } = 0L;
public bool HasTrash => TotalCount > 0;
public ImageSource EmptyIcon { get; set; } = _emptyIcon;
public ImageSource FullIcon { get; set; } = _fullIcon;
private static string FormatBytes(ulong bytes)
{
string[] sizes = ["B", "KB", "MB", "GB", "TB"];
double len = bytes;
int order = 0;
while (len >= 1024d && order < sizes.Length - 1)
{
order++;
len /= 1024d;
}
return $"{len:0.##} {sizes[order]}";
}
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct SHQUERYRBINFO
{
@@ -69,15 +171,21 @@ file static class RecycleBinHelper
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
private static extern int SHEmptyRecycleBin(nint hwnd, string pszRootPath, RecycleFlags dwFlags);
[DllImport("shell32.dll", CharSet = CharSet.Auto)]
private static extern int ExtractIconEx(string lpszFile, int nIconIndex, nint[] phiconLarge, nint[] phiconSmall, int nIcons);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool DestroyIcon(nint hIcon);
[Flags]
private enum RecycleFlags : uint
public enum RecycleFlags : uint
{
SHERB_NOCONFIRMATION = 0x00000001,
SHERB_NOPROGRESSUI = 0x00000002,
SHERB_NOSOUND = 0x00000004,
}
public static bool HasTrash()
public static RecycleBinInfo GetRecycleBinInfo()
{
var info = new SHQUERYRBINFO()
{
@@ -85,21 +193,98 @@ file static class RecycleBinHelper
};
int result = SHQueryRecycleBin(null, ref info);
if (result == 0) // S_OK
string[] icons = GetIcons();
ImageSource[] bitmapSources = [_emptyIcon, _fullIcon];
if (bitmapSources[0] is null || bitmapSources[1] is null)
{
return info.i64NumItems > 0;
bitmapSources[0] = ExtractIconBitmap(icons[0]);
bitmapSources[1] = ExtractIconBitmap(icons[1]);
}
// Fallback
return false;
if (result == 0 && icons.Length >= 2) // S_OK (0)
{
var output = new RecycleBinInfo()
{
TotalSize = info.i64Size,
TotalCount = info.i64NumItems,
EmptyIcon = bitmapSources[0],
FullIcon = bitmapSources[1],
};
return output;
}
return default;
}
public static bool EmptyRecycleBin()
public static bool EmptyRecycleBin(RecycleFlags flags = RecycleFlags.SHERB_NOSOUND | RecycleFlags.SHERB_NOCONFIRMATION | RecycleFlags.SHERB_NOPROGRESSUI)
{
int result = SHEmptyRecycleBin(IntPtr.Zero, null,
RecycleFlags.SHERB_NOCONFIRMATION | RecycleFlags.SHERB_NOPROGRESSUI | RecycleFlags.SHERB_NOSOUND);
int result = SHEmptyRecycleBin(IntPtr.Zero, null, flags);
return result == 0;
return result == 0; // S_OK (0)
}
private static string[] GetIcons()
{
const string keyPath = @"CLSID\{645FF040-5081-101B-9F08-00AA002F954E}\DefaultIcon";
using RegistryKey key = Registry.ClassesRoot.OpenSubKey(keyPath);
if (key != null)
{
if (key.GetValue("Empty") is string emptyIcon
&& key.GetValue("Full") is string fullIcon)
{
return [emptyIcon, fullIcon];
}
}
return null;
}
private static ImageSource ExtractIconBitmap(string resourcePath)
{
try
{
if (string.IsNullOrWhiteSpace(resourcePath)) return null;
string expanded = Environment.ExpandEnvironmentVariables(resourcePath);
string[] parts = expanded.Split(',');
if (parts.Length != 2 || !int.TryParse(parts[1], out int iconIndex)) return null;
string dllPath = parts[0];
nint[] icons = new nint[1];
int count = ExtractIconEx(dllPath, iconIndex, icons, null, 1);
if (count > 0 && icons[0] != IntPtr.Zero)
{
try
{
using Icon icon = Icon.FromHandle(icons[0]);
return ((Icon)icon.Clone()).ToImageSource();
}
finally
{
DestroyIcon(icons[0]);
}
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
return null;
}
private static ImageSource ToImageSource(this Icon icon)
{
var imageSource = Imaging.CreateBitmapSourceFromHIcon(
icon.Handle,
Int32Rect.Empty,
BitmapSizeOptions.FromEmptyOptions());
imageSource.Freeze();
return imageSource;
}
}

View File

@@ -15,14 +15,27 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
using QuickLook.Common.Plugin;
using System.Windows;
using System.Windows.Controls;
namespace QuickLook.Plugin.CLSIDViewer;
public partial class ThisPCPanel : UserControl
{
public ThisPCPanel()
private ContextObject _context;
public ThisPCPanel(ContextObject context)
{
_context = context;
DataContext = this;
InitializeComponent();
Loaded += OnLoaded;
}
protected virtual void OnLoaded(object sender, RoutedEventArgs e)
{
_context.IsBusy = false;
}
}

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<Translations>
<en>
<RecycleBinEmptyText>Recycle Bin is empty</RecycleBinEmptyText>
<RecycleBinButton>Empty Recycle Bin</RecycleBinButton>
<ConfirmDeleteText>Are you sure you want to permanently delete these {0} item(s)?</ConfirmDeleteText>
<RecycleBinSizeText>Total size {0}, {1} items</RecycleBinSizeText>
</en>
<zh-CN>
<RecycleBinEmptyText>回收站是空的</RecycleBinEmptyText>
<RecycleBinButton>清空回收站</RecycleBinButton>
<ConfirmDeleteText>您确定要永久删除这 {0} 个项目吗?</ConfirmDeleteText>
<RecycleBinSizeText>总大小 {0},共 {1} 个项目</RecycleBinSizeText>
</zh-CN>
<zh-TW>
<RecycleBinEmptyText>回收站是空的</RecycleBinEmptyText>
<RecycleBinButton>清空回收站</RecycleBinButton>
<ConfirmDeleteText>您確定要永久刪除這 {0} 個項目嗎?</ConfirmDeleteText>
<RecycleBinSizeText>總大小 {0},共 {1} 個項目</RecycleBinSizeText>
</zh-TW>
<ja>
<RecycleBinEmptyText>ごみ箱は空です</RecycleBinEmptyText>
<RecycleBinButton>ごみ箱を空にする</RecycleBinButton>
<ConfirmDeleteText>これらの {0} 項目を完全に削除してもよろしいですか?</ConfirmDeleteText>
<RecycleBinSizeText>合計サイズ {0}、{1} 項目</RecycleBinSizeText>
</ja>
<ko>
<RecycleBinEmptyText>휴지통이 비어 있습니다</RecycleBinEmptyText>
<RecycleBinButton>휴지통 비우기</RecycleBinButton>
<ConfirmDeleteText>{0}개의 항목을 영구적으로 삭제하시겠습니까?</ConfirmDeleteText>
<RecycleBinSizeText>총 크기 {0}, 총 {1}개 항목</RecycleBinSizeText>
</ko>
<fr>
<RecycleBinEmptyText>La corbeille est vide</RecycleBinEmptyText>
<RecycleBinButton>Vider la corbeille</RecycleBinButton>
<ConfirmDeleteText>Êtes-vous sûr de vouloir supprimer définitivement ces {0} élément(s) ?</ConfirmDeleteText>
<RecycleBinSizeText>Taille totale {0}, {1} éléments</RecycleBinSizeText>
</fr>
<de>
<RecycleBinEmptyText>Der Papierkorb ist leer</RecycleBinEmptyText>
<RecycleBinButton>Papierkorb leeren</RecycleBinButton>
<ConfirmDeleteText>Möchten Sie diese {0} Elemente wirklich dauerhaft löschen?</ConfirmDeleteText>
<RecycleBinSizeText>Gesamtgröße {0}, {1} Elemente</RecycleBinSizeText>
</de>
<es>
<RecycleBinEmptyText>La papelera está vacía</RecycleBinEmptyText>
<RecycleBinButton>Vaciar papelera</RecycleBinButton>
<ConfirmDeleteText>¿Está seguro de que desea eliminar permanentemente estos {0} elemento(s)?</ConfirmDeleteText>
<RecycleBinSizeText>Tamaño total {0}, {1} elementos</RecycleBinSizeText>
</es>
</Translations>