mirror of
https://github.com/QL-Win/QuickLook.git
synced 2025-09-11 09:49:07 +00:00
Improve UI: Refine Recycle Bin icon display #1610
This commit is contained in:
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -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()
|
||||
|
@@ -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>
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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>
|
Reference in New Issue
Block a user