diff --git a/QuickLook.Native/QuickLook.Native32/HelperMethods.cpp b/QuickLook.Native/QuickLook.Native32/HelperMethods.cpp index 011647472..54b0730b1 100644 --- a/QuickLook.Native/QuickLook.Native32/HelperMethods.cpp +++ b/QuickLook.Native/QuickLook.Native32/HelperMethods.cpp @@ -1,4 +1,4 @@ -// Copyright © 2017-2026 QL-Win Contributors +// Copyright © 2017-2026 QL-Win Contributors // // This file is part of QuickLook program. // @@ -33,6 +33,62 @@ void HelperMethods::GetSelectedInternal(CComPtr psb, PWCHAR buffe return ObtainFirstItem(dao, buffer); } +namespace +{ + void FormatVirtualItem(IShellItem* shellItem, PIDLIST_ABSOLUTE pidlFull, PCWSTR pszPath, PWCHAR buffer) + { + ATL::CComHeapPtr name; + if (SUCCEEDED(shellItem->GetDisplayName(SIGDN_NORMALDISPLAY, &name)) && name) + { + for (PWSTR p = name; *p; ++p) + if (*p == L'|') *p = L'_'; + } + + SFGAOF attribs = 0; + shellItem->GetAttributes(SFGAO_FOLDER, &attribs); + bool isFolder = (attribs & SFGAO_FOLDER) != 0; + + LONGLONG size = -1LL; + ULONGLONG ft = 0; + + CComQIPtr shellItem2(shellItem); + CComPtr store; + if (shellItem2 && SUCCEEDED(shellItem2->GetPropertyStore(GPS_FASTPROPERTIESONLY, IID_PPV_ARGS(&store)))) + { + if (!isFolder) + { + PROPVARIANT propSize = {}; + if (SUCCEEDED(store->GetValue(PKEY_Size, &propSize))) + { + if (propSize.vt == VT_UI8) + size = (LONGLONG)propSize.uhVal.QuadPart; + } + PropVariantClear(&propSize); + } + + PROPVARIANT propDate = {}; + if (SUCCEEDED(store->GetValue(PKEY_DateModified, &propDate))) + { + if (propDate.vt == VT_FILETIME) + { + ULARGE_INTEGER uli = { propDate.filetime.dwLowDateTime, propDate.filetime.dwHighDateTime }; + ft = uli.QuadPart; + } + } + PropVariantClear(&propDate); + } + + SHFILEINFOW sfi = {}; + int iconIndex = (pidlFull && SHGetFileInfoW((PCWSTR)pidlFull, 0, &sfi, sizeof(sfi), SHGFI_PIDL | SHGFI_SYSICONINDEX)) ? sfi.iIcon : -1; + + if (FAILED(StringCchPrintfW(buffer, MAX_PATH_EX, L"::QL_VIRTUAL|%lld|%llu|%d|%s|%s", + size, ft, iconIndex, name ? (PCWSTR)name : L"", pszPath))) + { + buffer[0] = L'\0'; + } + } +} + void HelperMethods::ObtainFirstItem(CComPtr dao, PWCHAR buffer) { if (!dao || !buffer) @@ -61,12 +117,17 @@ void HelperMethods::ObtainFirstItem(CComPtr dao, PWCHAR buffer) WCHAR localBuffer[MAX_PATH] = { '\0' }; if (DragQueryFileW(hDrop, 0, localBuffer, MAX_PATH) > 0) { - GetLongPathName(localBuffer, buffer, MAX_PATH_EX); + DWORD length = GetLongPathNameW(localBuffer, buffer, MAX_PATH_EX); + if (length == 0 || length >= MAX_PATH_EX) + { + if (FAILED(StringCchCopyW(buffer, MAX_PATH_EX, localBuffer))) + buffer[0] = L'\0'; + } ReleaseStgMedium(&medium); return; } - ReleaseStgMedium(&medium); } + ReleaseStgMedium(&medium); } // If CF_HDROP fails, try CFSTR_SHELLIDLIST @@ -78,8 +139,10 @@ void HelperMethods::ObtainFirstItem(CComPtr dao, PWCHAR buffer) if (SUCCEEDED(dao->GetData(&formatetc, &medium))) { CIDA* pida = (CIDA*)GlobalLock(medium.hGlobal); - if (!pida) + if (!pida || pida->cidl < 1) { + if (pida) + GlobalUnlock(medium.hGlobal); ReleaseStgMedium(&medium); return; } @@ -97,11 +160,26 @@ void HelperMethods::ObtainFirstItem(CComPtr dao, PWCHAR buffer) CComPtr shellItem; if (SUCCEEDED(SHCreateItemFromIDList(pidlFull, IID_PPV_ARGS(&shellItem)))) { - PWSTR pszPath = nullptr; - if (SUCCEEDED(shellItem->GetDisplayName(SIGDN_DESKTOPABSOLUTEPARSING, &pszPath))) + ATL::CComHeapPtr filePath; + if (SUCCEEDED(shellItem->GetDisplayName(SIGDN_FILESYSPATH, &filePath)) && filePath) + { + if (FAILED(StringCchCopyW(buffer, MAX_PATH_EX, filePath))) + buffer[0] = L'\0'; + } + else { - StringCchCopyW(buffer, MAX_PATH_EX, pszPath); // returns e.g., ::{645FF040-5081-101B-9F08-00AA002F954E} - CoTaskMemFree(pszPath); + ATL::CComHeapPtr parsingPath; + if (SUCCEEDED(shellItem->GetDisplayName(SIGDN_DESKTOPABSOLUTEPARSING, &parsingPath)) && parsingPath) + { + bool isPureClsid = wcslen(parsingPath) == 40 && parsingPath[0] == L':' && parsingPath[1] == L':' && parsingPath[2] == L'{' && parsingPath[39] == L'}'; + if (isPureClsid) + { + if (FAILED(StringCchCopyW(buffer, MAX_PATH_EX, parsingPath))) + buffer[0] = L'\0'; + } + else + FormatVirtualItem(shellItem, pidlFull, parsingPath, buffer); + } } } diff --git a/QuickLook.Native/QuickLook.Native32/Shell32.cpp b/QuickLook.Native/QuickLook.Native32/Shell32.cpp index 4c048ddec..cb7d54b06 100644 --- a/QuickLook.Native/QuickLook.Native32/Shell32.cpp +++ b/QuickLook.Native/QuickLook.Native32/Shell32.cpp @@ -95,8 +95,20 @@ Shell32::FocusedWindowType Shell32::GetFocusedWindowType() return INVALID; } +namespace +{ + struct ScopedComInit + { + HRESULT hr; + ScopedComInit() : hr(CoInitialize(nullptr)) {} + ~ScopedComInit() { if (SUCCEEDED(hr)) CoUninitialize(); } + }; +} + void Shell32::GetCurrentSelection(PWCHAR buffer) { + ScopedComInit com; + switch (GetFocusedWindowType()) { case DESKTOP: @@ -133,8 +145,6 @@ void Shell32::GetCurrentSelection(PWCHAR buffer) void Shell32::getSelectedFromExplorer(PWCHAR buffer) { - CoInitialize(nullptr); - CComPtr psw; if (FAILED(psw.CoCreateInstance(CLSID_ShellWindows))) return; @@ -181,8 +191,6 @@ void Shell32::getSelectedFromExplorer(PWCHAR buffer) void Shell32::getSelectedFromDesktop(PWCHAR buffer) { - CoInitialize(nullptr); - CComPtr psw; CComPtr pwba; diff --git a/QuickLook.Native/QuickLook.Native32/stdafx.h b/QuickLook.Native/QuickLook.Native32/stdafx.h index ef0cc6ae8..7f99b67a2 100644 --- a/QuickLook.Native/QuickLook.Native32/stdafx.h +++ b/QuickLook.Native/QuickLook.Native32/stdafx.h @@ -25,7 +25,9 @@ // TODO: reference additional headers your program requires here +#include #include +#include #include #include #include @@ -33,5 +35,6 @@ #include #include #include +#include #define MAX_PATH_EX 32767 diff --git a/QuickLook/FocusMonitor.cs b/QuickLook/FocusMonitor.cs index 1cb616b06..7cc4e955d 100644 --- a/QuickLook/FocusMonitor.cs +++ b/QuickLook/FocusMonitor.cs @@ -46,7 +46,7 @@ public void Start() continue; var path = NativeMethods.QuickLook.GetCurrentSelection(); - if (IsRunning && last != path) + if (IsRunning && !NativeMethods.VirtualItemInfo.IsSameItem(last, path)) { last = path; PipeServerManager.SendMessage(PipeMessages.Switch, path); diff --git a/QuickLook/NativeMethods/QuickLook.cs b/QuickLook/NativeMethods/QuickLook.cs index 0f6bfaf4e..e510d92d8 100644 --- a/QuickLook/NativeMethods/QuickLook.cs +++ b/QuickLook/NativeMethods/QuickLook.cs @@ -1,4 +1,4 @@ -// Copyright © 2017-2026 QL-Win Contributors +// Copyright © 2017-2026 QL-Win Contributors // // This file is part of QuickLook program. // @@ -17,6 +17,7 @@ using System; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; @@ -25,6 +26,61 @@ namespace QuickLook.NativeMethods; +internal readonly struct VirtualItemInfo +{ + public const string Prefix = "::QL_VIRTUAL|"; + private const long MaxFileTime = 2650467743999999999L; // DateTime.MaxValue.ToFileTime() + + public string DisplayName { get; } + public long? FileSize { get; } + public DateTime? DateModified { get; } + public int IconIndex { get; } + public string ParsingName { get; } + + public string EffectiveName => string.IsNullOrEmpty(DisplayName) ? ParsingName : DisplayName; + + public static bool IsVirtual(string path) => + !string.IsNullOrEmpty(path) && path.StartsWith(Prefix, StringComparison.Ordinal); + + public static bool TryParse(string path, out VirtualItemInfo info) + { + info = default; + if (!IsVirtual(path)) + return false; + + var parts = path.Substring(Prefix.Length).Split(new[] { '|' }, 5); + if (parts.Length < 5) + return false; + + var fileSize = long.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var size) && size >= 0 ? (long?)size : null; + _ = long.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var fileTime); + var iconIndex = int.TryParse(parts[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out var idx) ? idx : -1; + + DateTime? dt = (fileTime > 0 && fileTime <= MaxFileTime) + ? DateTime.FromFileTime(fileTime) : null; + + info = new VirtualItemInfo(parts[3], fileSize, dt, iconIndex, parts[4]); + return true; + } + + public static bool IsSameItem(string left, string right) + { + if (TryParse(left, out var a) && TryParse(right, out var b)) + return string.Equals(a.ParsingName, b.ParsingName, StringComparison.Ordinal); + + return string.Equals(left, right, StringComparison.Ordinal); + } + + private VirtualItemInfo(string displayName, long? fileSize, DateTime? dateModified, int iconIndex, string parsingName) + { + DisplayName = displayName; + FileSize = fileSize; + DateModified = dateModified; + IconIndex = iconIndex; + ParsingName = parsingName; + } +} + internal static class QuickLook { private const int MaxPath = 32767; @@ -120,13 +176,15 @@ internal static string GetCurrentSelection() thread.SetApartmentState(ApartmentState.STA); thread.Start(); thread.Join(); - if (sb.Length > 2 && sb[0] == '"' && sb[sb.Length - 1] == '"') - { - // We got a quoted string which breaks ResolveShortcut - sb.Remove(sb.Length - 1, 1); // remove last " - sb.Remove(0, 1); // remove first " - } - return ResolveShortcut(sb?.ToString() ?? string.Empty); + + var raw = sb.ToString(); + if (VirtualItemInfo.IsVirtual(raw)) + return raw; + + if (raw.Length >= 2 && raw.StartsWith("\"") && raw.EndsWith("\"")) + raw = raw.Substring(1, raw.Length - 2); + + return ResolveShortcut(raw); } private static string ResolveShortcut(string path) diff --git a/QuickLook/Plugin/InfoPanel/InfoPanel.xaml.cs b/QuickLook/Plugin/InfoPanel/InfoPanel.xaml.cs index e4c1ce1dd..d7be293bb 100644 --- a/QuickLook/Plugin/InfoPanel/InfoPanel.xaml.cs +++ b/QuickLook/Plugin/InfoPanel/InfoPanel.xaml.cs @@ -42,6 +42,24 @@ public bool Stop public void DisplayInfo(string path) { + image.Source = null; + + if (NativeMethods.VirtualItemInfo.TryParse(path, out var vInfo)) + { + filename.Text = vInfo.EffectiveName; + + modDate.Text = vInfo.DateModified is { } date + ? string.Format(TranslationHelper.Get("InfoPanel_LastModified"), date.ToString(CultureInfo.CurrentCulture)) + : string.Empty; + + totalSize.Text = vInfo.FileSize is long size ? size.ToPrettySize(2) : string.Empty; + + var icon = WindowsThumbnailProvider.GetJumboIcon(vInfo.IconIndex); + image.Source = icon; + image.Opacity = icon != null ? 1 : 0; + return; + } + _ = Task.Run(() => { var scale = DisplayDeviceHelper.GetCurrentScaleFactor(); diff --git a/QuickLook/Plugin/InfoPanel/WindowsThumbnailProvider.cs b/QuickLook/Plugin/InfoPanel/WindowsThumbnailProvider.cs index 7461d70c4..aeac210a1 100644 --- a/QuickLook/Plugin/InfoPanel/WindowsThumbnailProvider.cs +++ b/QuickLook/Plugin/InfoPanel/WindowsThumbnailProvider.cs @@ -20,6 +20,7 @@ using System.Drawing.Imaging; using System.IO; using System.Runtime.InteropServices; +using System.Windows.Media.Imaging; namespace QuickLook.Plugin.InfoPanel; @@ -39,6 +40,24 @@ internal enum ThumbnailOptions internal static class WindowsThumbnailProvider { private const string IShellItem2Guid = "7E9FB0D3-919F-4307-AB2E-9B1860310C93"; + private const int ShilJumbo = 0x4; + private const int IldTransparent = 0x00000001; + private static readonly Guid IidImageList = new("46EB5926-582E-4017-9FDF-E8998DAA0950"); + + [ComImport, Guid("46EB5926-582E-4017-9FDF-E8998DAA0950"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + private interface IImageList + { + [PreserveSig] int _0(); [PreserveSig] int _1(); [PreserveSig] int _2(); [PreserveSig] int _3(); + [PreserveSig] int _4(); [PreserveSig] int _5(); [PreserveSig] int _6(); + [PreserveSig] int GetIcon(int i, int flags, out IntPtr picon); + } + + [DllImport("shell32.dll", PreserveSig = true)] + private static extern int SHGetImageList(int imageList, ref Guid iid, out IImageList result); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool DestroyIcon(IntPtr icon); [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern int SHCreateItemFromParsingName( @@ -52,6 +71,32 @@ private static extern int SHCreateItemFromParsingName( [return: MarshalAs(UnmanagedType.Bool)] private static extern bool DeleteObject(nint hObject); + internal static BitmapSource GetJumboIcon(int iconIndex) + { + if (iconIndex < 0) + return null; + + IImageList imageList = null; + IntPtr icon = IntPtr.Zero; + try + { + var iid = IidImageList; + if (SHGetImageList(ShilJumbo, ref iid, out imageList) >= 0 && imageList?.GetIcon(iconIndex, IldTransparent, out icon) >= 0 && icon != IntPtr.Zero) + { + var source = System.Windows.Interop.Imaging.CreateBitmapSourceFromHIcon(icon, System.Windows.Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); + source.Freeze(); + return source; + } + } + finally + { + if (icon != IntPtr.Zero) DestroyIcon(icon); + if (imageList != null && Marshal.IsComObject(imageList)) Marshal.ReleaseComObject(imageList); + } + + return null; + } + public static Bitmap GetThumbnail(string fileName, int width, int height, ThumbnailOptions options) { var hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options); diff --git a/QuickLook/PluginManager.cs b/QuickLook/PluginManager.cs index 7560dcc6e..bde680e2e 100644 --- a/QuickLook/PluginManager.cs +++ b/QuickLook/PluginManager.cs @@ -54,6 +54,9 @@ internal IViewer FindMatch(string path) if (string.IsNullOrEmpty(path)) return null; + if (NativeMethods.VirtualItemInfo.IsVirtual(path)) + return DefaultPlugin.GetType().CreateInstance(); + var matched = GetInstance() .LoadedPlugins.FirstOrDefault(plugin => { diff --git a/QuickLook/ViewWindowManager.cs b/QuickLook/ViewWindowManager.cs index e18fc7004..4eaa128fa 100644 --- a/QuickLook/ViewWindowManager.cs +++ b/QuickLook/ViewWindowManager.cs @@ -1,4 +1,4 @@ -// Copyright © 2017-2026 QL-Win Contributors +// Copyright © 2017-2026 QL-Win Contributors // // This file is part of QuickLook program. // @@ -81,7 +81,7 @@ public void TogglePreview(string path = null, string options = null) if (!string.IsNullOrEmpty(options)) InvokePreviewWithOption(path, options); else - if (_viewerWindow.IsVisible && (string.IsNullOrEmpty(path) || path == _invokedPath)) + if (_viewerWindow.IsVisible && (string.IsNullOrEmpty(path) || NativeMethods.VirtualItemInfo.IsSameItem(path, _invokedPath))) ClosePreview(); else InvokePreview(path); @@ -140,24 +140,27 @@ public void InvokePreviewWithOption(string path = null, string options = null) } } + private static bool IsPureClsid(string path) => + path?.StartsWith("::{", StringComparison.Ordinal) == true && + Guid.TryParseExact(path.Substring(2), "B", out _); + + private static bool IsPhysicalPathPreviewable(string path) => + Directory.Exists(path) || (File.Exists(path) && ExtensionFilterHelper.IsExtensionAllowed(path)); + + private static bool IsPathPreviewable(string path) => + NativeMethods.VirtualItemInfo.IsVirtual(path) || + IsPureClsid(path) || + IsPhysicalPathPreviewable(path); + public void InvokePreview(string path = null) { if (string.IsNullOrEmpty(path)) path = NativeMethods.QuickLook.GetCurrentSelection(); - if (string.IsNullOrEmpty(path)) - return; - - if (_viewerWindow.IsVisible && path == _invokedPath) + if (!IsPathPreviewable(path)) return; - var isDirectory = Directory.Exists(path); - if (!isDirectory && !File.Exists(path)) - if (!path.StartsWith("::")) // CLSID - return; - - // Check extension filtering before proceeding (skip for directories) - if (!isDirectory && !ExtensionFilterHelper.IsExtensionAllowed(path)) + if (_viewerWindow.IsVisible && NativeMethods.VirtualItemInfo.IsSameItem(path, _invokedPath)) return; _invokedPath = path; @@ -174,16 +177,10 @@ public void InvokePluginPreview(string plugin, string path = null) if (string.IsNullOrEmpty(path)) path = _invokedPath; - if (string.IsNullOrEmpty(path)) - return; - - var isDirectory = Directory.Exists(path); - if (!isDirectory && !File.Exists(path)) + if (!IsPhysicalPathPreviewable(path)) return; - // Check extension filtering before proceeding (skip for directories) - if (!isDirectory && !ExtensionFilterHelper.IsExtensionAllowed(path)) - return; + _invokedPath = path; RunFocusMonitor(); @@ -200,7 +197,8 @@ public void InvokePluginPreview(string plugin, string path = null) public void ReloadPreview() { - if (!_viewerWindow.IsVisible || string.IsNullOrEmpty(_invokedPath)) + if (!_viewerWindow.IsVisible || string.IsNullOrEmpty(_invokedPath) + || NativeMethods.VirtualItemInfo.IsVirtual(_invokedPath)) return; var matchedPlugin = PluginManager.GetInstance().FindMatch(_invokedPath); @@ -229,17 +227,24 @@ private void CurrentPluginFailed(string path, ExceptionDispatchInfo e) _viewerWindow.Close(); - TrayIconManager.ShowNotification($"Failed to preview {Path.GetFileName(path)}", + var name = NativeMethods.VirtualItemInfo.TryParse(path, out var vInfo) + ? vInfo.EffectiveName + : (path?.StartsWith("::") == true ? path : (path != null ? Path.GetFileName(path) : string.Empty)); + + TrayIconManager.ShowNotification($"Failed to preview {name}", "Consider reporting this incident to QuickLook’s author.", true); Debug.WriteLine(e.SourceException.ToString()); - ProcessHelper.WriteLog(e.SourceException.ToString()); - if (plugin != PluginManager.GetInstance().DefaultPlugin.GetType()) - BeginShowNewWindow(path, PluginManager.GetInstance().DefaultPlugin); - else - e.Throw(); + if (plugin == typeof(Plugin.InfoPanel.Plugin)) + { + if (!NativeMethods.VirtualItemInfo.IsVirtual(path)) + e.Throw(); + return; + } + + BeginShowNewWindow(path, PluginManager.GetInstance().DefaultPlugin); } private void InitNewViewerWindow() diff --git a/QuickLook/ViewerWindow.Actions.cs b/QuickLook/ViewerWindow.Actions.cs index c5fb50506..822ea71c0 100644 --- a/QuickLook/ViewerWindow.Actions.cs +++ b/QuickLook/ViewerWindow.Actions.cs @@ -1,4 +1,4 @@ -// Copyright © 2017-2026 QL-Win Contributors +// Copyright © 2017-2026 QL-Win Contributors // // This file is part of QuickLook program. // @@ -45,7 +45,7 @@ public partial class ViewerWindow { internal void Run() { - if (string.IsNullOrEmpty(_path)) + if (string.IsNullOrEmpty(_path) || NativeMethods.VirtualItemInfo.IsVirtual(_path)) return; try @@ -364,6 +364,7 @@ internal void BeginShow(IViewer matchedPlugin, string path, // Initial the more menu ClearMoreMenuUnpin(); + if (NativeMethods.VirtualItemInfo.IsVirtual(_path)) return; foreach (var plugin in PluginManager.GetInstance().LoadedPlugins .GroupBy(x => x.ToString()).Select(g => g.First()) // DistinctBy plugin name @@ -480,8 +481,16 @@ private object ResolveIcon(object icon) private void SetOpenWithButtonAndPath() { - // Share icon - buttonShare.Visibility = ShareHelper.IsShareSupported(_path) ? Visibility.Visible : Visibility.Collapsed; + var isVirtual = NativeMethods.VirtualItemInfo.IsVirtual(_path); + + buttonReload.Visibility = !isVirtual && SettingHelper.Get("ShowReload", false) ? Visibility.Visible : Visibility.Collapsed; + buttonMore.Visibility = !isVirtual ? Visibility.Visible : Visibility.Collapsed; + buttonOpen.Visibility = !isVirtual ? Visibility.Visible : Visibility.Collapsed; + buttonOpenWith.Visibility = !isVirtual ? Visibility.Visible : Visibility.Collapsed; + buttonShare.Visibility = !isVirtual && ShareHelper.IsShareSupported(_path) ? Visibility.Visible : Visibility.Collapsed; + + if (isVirtual) + return; // Open icon if (Directory.Exists(_path))