diff --git a/QuickLook.Plugin/QuickLook.Plugin.CLSIDViewer/RecycleBinPanel.xaml.cs b/QuickLook.Plugin/QuickLook.Plugin.CLSIDViewer/RecycleBinPanel.xaml.cs
index e52a85cb1..398a2c2e5 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.CLSIDViewer/RecycleBinPanel.xaml.cs
+++ b/QuickLook.Plugin/QuickLook.Plugin.CLSIDViewer/RecycleBinPanel.xaml.cs
@@ -69,23 +69,104 @@ protected virtual void OnLoaded(object sender, RoutedEventArgs e)
});
}
- private async Task OnEmptyRecycleBinAsync()
+ private Task OnEmptyRecycleBinAsync()
{
+ var domain = Assembly.GetExecutingAssembly().GetName().Name;
+
var result = MessageBox.Show(
- string.Format(TranslationHelper.Get("ConfirmDeleteText", domain: Assembly.GetExecutingAssembly().GetName().Name), _info.TotalCount),
- TranslationHelper.Get("RecycleBinButton", domain: Assembly.GetExecutingAssembly().GetName().Name),
+ string.Format(TranslationHelper.Get("ConfirmDeleteText", domain: domain), _info.TotalCount),
+ TranslationHelper.Get("RecycleBinButton", domain: domain),
MessageBoxButton.YesNo, MessageBoxImage.Warning);
- if (result == MessageBoxResult.Yes)
+ if (result != MessageBoxResult.Yes)
+ return Task.CompletedTask;
+
+ // The emptying itself runs in a detached QuickLook.exe helper process, so it keeps
+ // running even when this viewer window is closed or another file gets previewed.
+ // This panel only reflects the state; it must never wait for the deletion itself.
+ emptyButton.IsEnabled = false;
+ totalSizeAndCount.Text = TranslationHelper.Get(
+ "RecycleBinEmptyingText",
+ domain: domain,
+ failsafe: "Emptying the Recycle Bin in the background...");
+
+ Process helperProcess;
+ try
{
- await Task.Run(() =>
+ helperProcess = Process.Start(new ProcessStartInfo
{
- if (RecycleBinHelper.EmptyRecycleBin())
- {
- UpdateState();
- }
+ FileName = Process.GetCurrentProcess().MainModule.FileName,
+ Arguments = "--empty-recycle-bin",
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ WorkingDirectory = AppDomain.CurrentDomain.BaseDirectory,
});
+
+ if (helperProcess == null)
+ throw new InvalidOperationException("The helper process could not be started.");
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine(ex);
+
+ MessageBox.Show(
+ "QuickLook could not start the background process to empty the Recycle Bin."
+ + Environment.NewLine + Environment.NewLine + ex.Message,
+ TranslationHelper.Get("RecycleBinButton", domain: domain),
+ MessageBoxButton.OK, MessageBoxImage.Error);
+
+ emptyButton.IsEnabled = true;
+ UpdateState();
+ return Task.CompletedTask;
}
+
+ // Never block the UI thread. Waiting is only used to refresh the panel once the
+ // helper is done; the window (and this plugin instance) may be gone by then.
+ _ = Task.Run(() =>
+ {
+ try
+ {
+ helperProcess.WaitForExit();
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine(ex);
+ }
+ finally
+ {
+ try
+ {
+ helperProcess.Dispose();
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine(ex);
+ }
+ }
+
+ try
+ {
+ Dispatcher?.BeginInvoke(new Action(() =>
+ {
+ try
+ {
+ UpdateState();
+ emptyButton.IsEnabled = true;
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine(ex);
+ }
+ }));
+ }
+ catch (Exception ex)
+ {
+ // The viewer window may have been closed while the helper was running.
+ Debug.WriteLine(ex);
+ }
+ });
+
+ return Task.CompletedTask;
}
private void UpdateState()
@@ -169,23 +250,12 @@ public struct SHQUERYRBINFO
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
private static extern int SHQueryRecycleBin(string pszRootPath, ref SHQUERYRBINFO pSHQueryRBInfo);
- [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]
- public enum RecycleFlags : uint
- {
- SHERB_NOCONFIRMATION = 0x00000001,
- SHERB_NOPROGRESSUI = 0x00000002,
- SHERB_NOSOUND = 0x00000004,
- }
-
public static RecycleBinInfo GetRecycleBinInfo()
{
var info = new SHQUERYRBINFO()
@@ -237,13 +307,6 @@ public static RecycleBinInfo GetRecycleBinInfo()
return default;
}
- public static bool EmptyRecycleBin(RecycleFlags flags = RecycleFlags.SHERB_NOSOUND | RecycleFlags.SHERB_NOCONFIRMATION | RecycleFlags.SHERB_NOPROGRESSUI)
- {
- int result = SHEmptyRecycleBin(IntPtr.Zero, null, flags);
-
- return result == 0; // S_OK (0)
- }
-
private static string[] GetIcons()
{
const string keyPath = @"CLSID\{645FF040-5081-101B-9F08-00AA002F954E}\DefaultIcon";
diff --git a/QuickLook.Plugin/QuickLook.Plugin.CLSIDViewer/Translations.config b/QuickLook.Plugin/QuickLook.Plugin.CLSIDViewer/Translations.config
index 6bb8817de..41d96e5e4 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.CLSIDViewer/Translations.config
+++ b/QuickLook.Plugin/QuickLook.Plugin.CLSIDViewer/Translations.config
@@ -6,173 +6,202 @@
إفراغ سلة المحذوفات
هل أنت متأكد أنك تريد حذف هذه العناصر {0} نهائيًا؟
الحجم الكلي {0}، عدد العناصر {1}
+ يتم إفراغ سلة المحذوفات في الخلفية…
La paperera està buida
Buida la paperera
Segur que vols eliminar definitivament aquests {0} elements?
Mida total {0}, {1} elements
+ S'està buidant la paperera en segon pla…
Ο κάδος ανακύκλωσης είναι άδειος
Άδειασμα κάδου ανακύκλωσης
Είστε βέβαιοι ότι θέλετε να διαγράψετε οριστικά αυτά τα {0} στοιχεία;
Συνολικό μέγεθος {0}, {1} στοιχεία
+ Άδειασμα του κάδου ανακύκλωσης στο παρασκήνιο…
סל המחזור ריק
רוקן את סל המחזור
האם אתה בטוח שברצונך למחוק לצמיתות את {0} פריטים אלה?
גודל כולל {0}, {1} פריטים
+ ריקון סל המחזור ברקע…
रीसायकल बिन खाली है
रीसायकल बिन खाली करें
क्या आप वाकई इन {0} आइटम्स को स्थायी रूप से हटाना चाहते हैं?
कुल आकार {0}, {1} आइटम्स
+ रीसायकल बिन पृष्ठभूमि में खाली किया जा रहा है…
A lomtár üres
Lomtár ürítése
Biztosan törölni szeretné ezt a(z) {0} elemet véglegesen?
Teljes méret {0}, {1} elem
+ A lomtár ürítése a háttérben…
Tempat Sampah kosong
Kosongkan Tempat Sampah
Apakah Anda yakin ingin menghapus {0} item ini secara permanen?
Ukuran total {0}, {1} item
+ Mengosongkan Tempat Sampah di latar belakang…
Il cestino è vuoto
Svuota cestino
Sei sicuro di voler eliminare definitivamente questi {0} elementi?
Dimensione totale {0}, {1} elementi
+ Svuotamento del cestino in background…
पुनःप्राप्ती बिन रिकामी आहे
पुनःप्राप्ती बिन रिकामा करा
आपण खरोखरच ही {0} आयटम्स कायमचे हटवू इच्छिता?
एकूण आकार {0}, {1} आयटम्स
+ पुनःप्राप्ती बिन पार्श्वभूमीत रिकामी होत आहे…
Papirkurven er tom
Tøm papirkurven
Er du sikker på at du vil slette disse {0} elementene permanent?
Total størrelse {0}, {1} elementer
+ Tømmer papirkurven i bakgrunnen…
Prullenbak is leeg
Prullenbak legen
Weet je zeker dat je deze {0} items permanent wilt verwijderen?
Totale grootte {0}, {1} items
+ Prullenbak wordt op de achtergrond geleegd…
Kosz jest pusty
Opróżnij kosz
Czy na pewno chcesz trwale usunąć te {0} elementy?
Całkowity rozmiar {0}, {1} elementów
+ Opróżnianie kosza w tle…
Lixeira está vazia
Esvaziar lixeira
Tem certeza de que deseja excluir permanentemente estes {0} itens?
Tamanho total {0}, {1} itens
+ Esvaziando a Lixeira em segundo plano…
A reciclagem está vazia
Esvaziar reciclagem
Tem a certeza que pretende eliminar permanentemente estes {0} itens?
Tamanho total {0}, {1} itens
+ A esvaziar a Reciclagem em segundo plano…
Корзина пуста
Очистить корзину
Вы уверены, что хотите навсегда удалить эти {0} элементов?
Общий размер {0}, {1} элементов
+ Очистка корзины в фоновом режиме…
Kôš je prázdny
Vyprázdniť kôš
Ste si istý, že chcete natrvalo odstrániť týchto {0} položiek?
Celková veľkosť {0}, {1} položiek
+ Kôš sa vyprázdňuje na pozadí…
Geri Dönüşüm Kutusu boş
Geri Dönüşüm Kutusunu Boşalt
Bu {0} öğeyi kalıcı olarak silmek istediğinizden emin misiniz?
Toplam boyut {0}, {1} öğe
+ Geri Dönüşüm Kutusu arka planda boşaltılıyor…
Кошик порожній
Очистити кошик
Ви впевнені, що хочете назавжди видалити ці {0} елементів?
Загальний розмір {0}, {1} елементів
+ Очищення кошика у фоновому режимі…
Thùng rác trống
Làm trống thùng rác
Bạn có chắc muốn xóa vĩnh viễn {0} mục này không?
Tổng kích thước {0}, {1} mục
+ Đang dọn Thùng rác trong nền…
Recycle Bin is empty
Empty Recycle Bin
Are you sure you want to permanently delete these {0} item(s)?
Total size {0}, {1} items
+ Emptying the Recycle Bin in the background…
휴지통이 비어 있습니다
휴지통 비우기
{0}개 항목을 영구적으로 삭제하시겠습니까?
전체 크기 {0}, {1}개 항목
+ 휴지통을 백그라운드에서 비우는 중…
回收站为空
清空回收站
您确定要永久删除这 {0} 个项目吗?
总大小 {0},共 {1} 个项目
+ 正在后台清空回收站…
回收站是空的
清空回收站
您確定要永久刪除這 {0} 個項目嗎?
總大小 {0},共 {1} 個項目
+ 正在背景清空資源回收筒…
ごみ箱は空です
ごみ箱を空にする
これらの {0} 項目を完全に削除してもよろしいですか?
合計サイズ {0}、{1} 項目
+ ごみ箱をバックグラウンドで空にしています…
La corbeille est vide
Vider la corbeille
Êtes-vous sûr de vouloir supprimer définitivement ces {0} élément(s) ?
Taille totale {0}, {1} éléments
+ Vidage de la corbeille en arrière-plan…
Der Papierkorb ist leer
Papierkorb leeren
Möchten Sie diese {0} Elemente wirklich dauerhaft löschen?
Gesamtgröße {0}, {1} Elemente
+ Papierkorb wird im Hintergrund geleert…
La papelera está vacía
Vaciar papelera
¿Está seguro de que desea eliminar permanentemente estos {0} elemento(s)?
Tamaño total {0}, {1} elementos
+ Vaciando la papelera en segundo plano…
Papperskorgen är tom
Töm papperskorgen
Vill du ta bort de här {0} objekten permanent?
Total storlek: {0} ({1} objekt)
+ Tömmer papperskorgen i bakgrunden…
Coșul de reciclare este gol
Golire Coș de reciclare
Sunteți sigur că doriți să ștergeți definitiv aceste {0} elemente?
Dimensiune totală {0}, {1} elemente
+ Se golește coșul de reciclare în fundal…
diff --git a/QuickLook/App.xaml.cs b/QuickLook/App.xaml.cs
index 838e774e7..143c30a7e 100644
--- a/QuickLook/App.xaml.cs
+++ b/QuickLook/App.xaml.cs
@@ -133,6 +133,27 @@ static IEnumerable FetchFiles(string rootPath, string targetFileName)
protected override void OnStartup(StartupEventArgs e)
{
+ // Hidden background mode: empty the Recycle Bin in a detached process, so the
+ // deletion keeps running after the viewer window is closed, after another file
+ // gets previewed, or after Space is pressed again.
+ // It must be handled before any instance check, tray icon or window creation.
+ if (e.Args.Any(arg => string.Equals(arg, "--empty-recycle-bin", StringComparison.OrdinalIgnoreCase)))
+ {
+ bool emptied = RecycleBinCleaner.EmptyRecycleBin(out int hresult);
+
+ try
+ {
+ ProcessHelper.WriteLog($"[RecycleBinCleaner] empty result={emptied} hresult=0x{hresult:X8}");
+ }
+ catch
+ {
+ // Logging must never change the helper's outcome.
+ }
+
+ Environment.Exit(emptied ? 0 : 1);
+ return;
+ }
+
if (!EnsureOSVersion()
|| !EnsureFirstInstance(e.Args)
|| !EnsureFolderWritable(SettingHelper.LocalDataPath))
diff --git a/QuickLook/Helpers/RecycleBinCleaner.cs b/QuickLook/Helpers/RecycleBinCleaner.cs
new file mode 100644
index 000000000..f6d78e6e9
--- /dev/null
+++ b/QuickLook/Helpers/RecycleBinCleaner.cs
@@ -0,0 +1,185 @@
+// Copyright © 2017-2026 QL-Win Contributors
+//
+// This file is part of QuickLook program.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+using System;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using System.Threading;
+
+namespace QuickLook.Helpers;
+
+///
+/// The actual Recycle Bin emptying routine used by the hidden `--empty-recycle-bin`
+/// command line mode of QuickLook.exe. The plugin launches that mode as a detached
+/// process, so emptying keeps running independently of the viewer window and of
+/// whether another file gets previewed.
+///
+public static class RecycleBinCleaner
+{
+ ///
+ /// Cross-process mutex which prevents two helper processes (for example, one
+ /// started by a movie-preview instance and one by the main instance) from
+ /// running a concurrent empty operation.
+ ///
+ public const string EmptyingMutexName = @"QuickLook.RecycleBin.Emptying";
+
+ [Flags]
+ private enum RecycleBinFlags : uint
+ {
+ SHERB_NOCONFIRMATION = 0x00000001,
+ SHERB_NOPROGRESSUI = 0x00000002,
+ SHERB_NOSOUND = 0x00000004,
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ private struct SHQUERYRBINFO
+ {
+ public uint cbSize;
+ public ulong i64Size;
+ public ulong i64NumItems;
+ }
+
+ [DllImport("shell32.dll", CharSet = CharSet.Unicode)]
+ private static extern int SHEmptyRecycleBin(IntPtr hwnd, string pszRootPath, RecycleBinFlags dwFlags);
+
+ [DllImport("shell32.dll", CharSet = CharSet.Unicode)]
+ private static extern int SHQueryRecycleBin(string pszRootPath, ref SHQUERYRBINFO pSHQueryRBInfo);
+
+ ///
+ /// Empties all Recycle Bin locations without any user interface.
+ ///
+ ///
+ /// true when the Recycle Bin was emptied (or another helper process was already
+ /// doing it); false when Windows reported a failure.
+ ///
+ public static bool EmptyRecycleBin()
+ {
+ return EmptyRecycleBin(out _);
+ }
+
+ ///
+ /// Empties all Recycle Bin locations without any user interface.
+ ///
+ /// The HRESULT returned by SHEmptyRecycleBin (0 on success).
+ ///
+ /// true when the Recycle Bin was emptied (or another helper process was already
+ /// doing it); false when Windows reported a failure.
+ ///
+ public static bool EmptyRecycleBin(out int hresult)
+ {
+ hresult = 0;
+
+ Mutex mutex = null;
+ var ownsMutex = false;
+
+ try
+ {
+ mutex = new Mutex(false, EmptyingMutexName);
+
+ try
+ {
+ // Do not wait: a second helper should exit immediately instead of
+ // starting a concurrent empty operation.
+ ownsMutex = mutex.WaitOne(0);
+ }
+ catch (AbandonedMutexException)
+ {
+ // The previous helper terminated while emptying. Take over.
+ ownsMutex = true;
+ }
+
+ if (!ownsMutex)
+ {
+ // Another helper is already emptying the Recycle Bin.
+ return true;
+ }
+
+ // SHEmptyRecycleBin returns E_UNEXPECTED (0x8000FFFF) when the Recycle Bin is
+ // already empty; that is not a failure for our background job.
+ if (TryGetRecycleBinInfo(out _, out var itemCount) && itemCount == 0)
+ {
+ hresult = 0;
+ return true;
+ }
+
+ hresult = SHEmptyRecycleBin(
+ IntPtr.Zero,
+ null,
+ RecycleBinFlags.SHERB_NOCONFIRMATION |
+ RecycleBinFlags.SHERB_NOPROGRESSUI |
+ RecycleBinFlags.SHERB_NOSOUND);
+
+ // S_FALSE (1) means "nothing was deleted"; both results are a success for us.
+ return hresult == 0 || hresult == 1;
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine(ex);
+ hresult = Marshal.GetHRForException(ex);
+ return false;
+ }
+ finally
+ {
+ if (ownsMutex)
+ {
+ try
+ {
+ mutex.ReleaseMutex();
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine(ex);
+ }
+ }
+
+ mutex?.Dispose();
+ }
+ }
+
+ ///
+ /// Queries the current Recycle Bin size and item count.
+ ///
+ /// Total size in bytes.
+ /// Number of items.
+ /// true when the query succeeded.
+ public static bool TryGetRecycleBinInfo(out ulong totalSize, out ulong totalCount)
+ {
+ totalSize = 0;
+ totalCount = 0;
+
+ try
+ {
+ var info = new SHQUERYRBINFO
+ {
+ cbSize = (uint)Marshal.SizeOf(typeof(SHQUERYRBINFO)),
+ };
+
+ if (SHQueryRecycleBin(null, ref info) != 0) // S_OK (0)
+ return false;
+
+ totalSize = info.i64Size;
+ totalCount = info.i64NumItems;
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine(ex);
+ return false;
+ }
+ }
+}