Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 86 additions & 8 deletions QuickLook.Native/QuickLook.Native32/HelperMethods.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2017-2026 QL-Win Contributors
// Copyright © 2017-2026 QL-Win Contributors
//
// This file is part of QuickLook program.
//
Expand Down Expand Up @@ -33,6 +33,62 @@ void HelperMethods::GetSelectedInternal(CComPtr<IShellBrowser> psb, PWCHAR buffe
return ObtainFirstItem(dao, buffer);
}

namespace
{
void FormatVirtualItem(IShellItem* shellItem, PIDLIST_ABSOLUTE pidlFull, PCWSTR pszPath, PWCHAR buffer)
{
ATL::CComHeapPtr<WCHAR> 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<IShellItem2> shellItem2(shellItem);
CComPtr<IPropertyStore> 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<IDataObject> dao, PWCHAR buffer)
{
if (!dao || !buffer)
Expand Down Expand Up @@ -61,12 +117,17 @@ void HelperMethods::ObtainFirstItem(CComPtr<IDataObject> 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
Expand All @@ -78,8 +139,10 @@ void HelperMethods::ObtainFirstItem(CComPtr<IDataObject> 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;
}
Expand All @@ -97,11 +160,26 @@ void HelperMethods::ObtainFirstItem(CComPtr<IDataObject> dao, PWCHAR buffer)
CComPtr<IShellItem> shellItem;
if (SUCCEEDED(SHCreateItemFromIDList(pidlFull, IID_PPV_ARGS(&shellItem))))
{
PWSTR pszPath = nullptr;
if (SUCCEEDED(shellItem->GetDisplayName(SIGDN_DESKTOPABSOLUTEPARSING, &pszPath)))
ATL::CComHeapPtr<WCHAR> 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<WCHAR> 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);
}
}
}

Expand Down
16 changes: 12 additions & 4 deletions QuickLook.Native/QuickLook.Native32/Shell32.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -133,8 +145,6 @@ void Shell32::GetCurrentSelection(PWCHAR buffer)

void Shell32::getSelectedFromExplorer(PWCHAR buffer)
{
CoInitialize(nullptr);

CComPtr<IShellWindows> psw;
if (FAILED(psw.CoCreateInstance(CLSID_ShellWindows)))
return;
Expand Down Expand Up @@ -181,8 +191,6 @@ void Shell32::getSelectedFromExplorer(PWCHAR buffer)

void Shell32::getSelectedFromDesktop(PWCHAR buffer)
{
CoInitialize(nullptr);

CComPtr<IShellWindows> psw;
CComPtr<IWebBrowserApp> pwba;

Expand Down
3 changes: 3 additions & 0 deletions QuickLook.Native/QuickLook.Native32/stdafx.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@


// TODO: reference additional headers your program requires here
#include<atlbase.h>
#include<atlcomcli.h>
#include<atlalloc.h>
#include<Exdisp.h>
#include<Shobjidl.h>
#include<shlguid.h>
#include<Shlobj.h>
#include<Shellapi.h>
#include<Psapi.h>
#include<AppModel.h>
#include<propkey.h>

#define MAX_PATH_EX 32767
2 changes: 1 addition & 1 deletion QuickLook/FocusMonitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
74 changes: 66 additions & 8 deletions QuickLook/NativeMethods/QuickLook.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2017-2026 QL-Win Contributors
// Copyright © 2017-2026 QL-Win Contributors
//
// This file is part of QuickLook program.
//
Expand All @@ -17,6 +17,7 @@

using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
Expand All @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
18 changes: 18 additions & 0 deletions QuickLook/Plugin/InfoPanel/InfoPanel.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
45 changes: 45 additions & 0 deletions QuickLook/Plugin/InfoPanel/WindowsThumbnailProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows.Media.Imaging;

namespace QuickLook.Plugin.InfoPanel;

Expand All @@ -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(
Expand All @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions QuickLook/PluginManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ internal IViewer FindMatch(string path)
if (string.IsNullOrEmpty(path))
return null;

if (NativeMethods.VirtualItemInfo.IsVirtual(path))
return DefaultPlugin.GetType().CreateInstance<IViewer>();

var matched = GetInstance()
.LoadedPlugins.FirstOrDefault(plugin =>
{
Expand Down
Loading