From 12aaf22dd0f7cd4622d4883fb2c62ddbb0902a44 Mon Sep 17 00:00:00 2001 From: mohamadsm313m-prog Date: Fri, 13 Nov 2026 17:00:15 +0330 Subject: [PATCH] Empty the Recycle Bin in a detached background process SHEmptyRecycleBin used to run inside the viewer process, so closing the preview window or previewing another file aborted the deletion. The panel now starts QuickLook.exe with a hidden --empty-recycle-bin argument; that helper empties the bin with no UI, persists after the viewer is gone and is guarded by a cross-process mutex. The panel shows an emptying state and refreshes itself afterwards. --- .../RecycleBinPanel.xaml.cs | 117 ++++++++--- .../Translations.config | 29 +++ QuickLook/App.xaml.cs | 21 ++ QuickLook/Helpers/RecycleBinCleaner.cs | 185 ++++++++++++++++++ 4 files changed, 325 insertions(+), 27 deletions(-) create mode 100644 QuickLook/Helpers/RecycleBinCleaner.cs 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; + } + } +}