Skip to content

feat(preview-poc): Windows Explorer thumbnail provider (#935)#936

Closed
codemonkey85 wants to merge 11 commits into
mainfrom
feat/windows-thumbnail-provider
Closed

feat(preview-poc): Windows Explorer thumbnail provider (#935)#936
codemonkey85 wants to merge 11 commits into
mainfrom
feat/windows-thumbnail-provider

Conversation

@codemonkey85
Copy link
Copy Markdown
Owner

@codemonkey85 codemonkey85 commented May 26, 2026

Adds thumbnail providers for both Windows Explorer and macOS Finder — icon thumbnails for PKHeX files. Part of #935.

Windows (IThumbnailProvider)

Reuses the same native-shim + worker split and bundled sprites as the preview handler from #934:

  • Pkmds.Preview.FileSprite (shared) — picks the representative sprite for a file: entity → its species, save → lead party Pokémon (then first box slot, then placeholder), mystery gift → species/item.
  • Worker --thumbnail <out.png> <size> <file> — draws the sprite onto a square transparent PNG (System.Drawing), scaled/centered.
  • PkmdsPreviewShim hosts a second COM class (IThumbnailProvider, CLSID {b98dbf6e-…}) alongside the preview handler. GetThumbnail(cx) spawns the worker, waits, and decodes the PNG into an alpha HBITMAP via WIC.
  • register.cs registers the thumbnail CLSID per extension (same shim DLL as the preview handler).

macOS (QLThumbnailProvider)

  • ThumbnailProvider.swift — Quick Look extension that calls into the AOT-compiled PkmdsNative.dylib to resolve the sprite path or describe the save file.
  • Pokémon files (.pk*, .pb*, .bk4, etc.) render the species sprite, trimmed of transparent padding via opaqueSourceRect and scaled to fill 90% of the context.
  • Save files (.gci, .dsv, .srm) render a trainer card showing game version (accent-coloured), OT name, TID/SID, and playtime.
  • Sprites are bundled into the extension at build time by build-extension.sh.
  • Context-size bug fixed: the QLThumbnailReply closure uses ctx.width/height (pixel dimensions) rather than request.maximumSize (points), doubling the effective fill on Retina displays.

Known limitation

.sav files are a prohibited extension on macOS — the system will not dispatch Quick Look or thumbnail requests to sandboxed extensions for them. .gci, .dsv, and .srm work automatically. A Spotlight MDImporter to assign the com.bondcodes.pkmds.save-file UTI to .sav files is tracked separately in #938.

Verified

  • ✅ macOS: species sprites render full-size and centred in Finder icon view (256 px and 512 px).
  • ✅ macOS: save-file trainer cards render for .gci/.dsv/.srm in Finder.
  • ✅ macOS: Quick Look spacebar preview works for .pk* and .gci/.dsv/.srm files.
  • ⏳ Windows: manual Explorer verification pending (needs elevated install.ps1 + thumbnail-cache refresh).

🤖 Generated with Claude Code

Adds an IThumbnailProvider alongside the preview handler — Explorer/Finder-style icon
thumbnails for PKHeX files — reusing the same native-shim + worker split and bundled sprites.

- Pkmds.Preview.FileSprite: shared "representative sprite for a file" (entity -> species,
  save -> lead party Pokemon, gift -> species/item, fallback), the thumbnail counterpart to
  HtmlRenderer.RenderFile.
- Worker --thumbnail mode: draws the bundled sprite to a square PNG (System.Drawing), offline.
- PkmdsPreviewShim now hosts a second COM class (IThumbnailProvider, separate CLSID
  {b98dbf6e-...}) that spawns the worker in --thumbnail mode and decodes its PNG into an alpha
  HBITMAP via WIC. DllGetClassObject dispatches both CLSIDs (templated class factory).
- register.cs registers the thumbnail CLSID + the IThumbnailProvider IID {e357fccd-...} per
  extension (same shim DLL).

Verified the worker renders correct thumbnails (.pk5 -> species, save -> lead party mon).
macOS/iOS QLThumbnailProvider extensions are deferred (need a Mac) — tracked in #935.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
codemonkey85 and others added 6 commits May 26, 2026 15:23
install.ps1 wiped+republished dist in step 2 but only stopped prevhost/Explorer in
step 5, so a live prevhost/dllhost/worker kept dist DLLs locked (Access denied on
dist\Accessibility.dll). Stop prevhost/dllhost/PkmdsPreviewWorker up front, before the
clean+publish.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The thumbnail cache (dllhost) initializes IThumbnailProvider handlers via
IInitializeWithStream and ignores a file-only handler, so thumbnails came up blank.
Implement IInitializeWithStream as the primary init (keep IInitializeWithFile as a
fallback); GetThumbnail spills the stream to a temp file for the worker. A stream has
no extension, so the worker content-detects entities/saves (gifts, which need the
extension, fall back to the placeholder in the stream path).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The thumbnail cache inits via IInitializeWithStream (no path), so the worker got a
temp file with no extension and couldn't identify mystery gifts (which are detected by
extension) — .wc6/.pgf/etc. fell back to the placeholder. Read the original name from
IStream::Stat (pwcsName) and reuse its extension for the temp file, so gift thumbnails
(e.g. a .wc6 Bulbasaur) resolve correctly. Falls back to .bin (content detection) if the
stream has no name.

Note: .wc3 (WonderCard3) only exposes Type, not the gifted species/item, so it still
shows the placeholder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WonderCard3 only exposes Type (Pokémon / Item / Link), not the gifted species/item, so
.wc3 thumbnails now return the item placeholder for Item-type cards and the Pokémon
placeholder otherwise (previously always the Pokémon placeholder).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Save files now thumbnail as a compact "trainer card" (game version code,
OT name, TID/SID, playtime) instead of the lead party Pokémon's sprite —
identifying both the game and the specific playthrough at a glance.

- SaveCard.Render draws the card with System.Drawing (no WebView2) so a
  folder of saves thumbnails quickly.
- Per-version accent colour; the two groups the save format can't
  disambiguate (RB = Red/Blue, GS = Gold/Silver) use a diagonal gradient
  border and a horizontal gradient on the code so each letter lands on its
  game's colour (R red, B blue; G gold, S silver).
- Thumbnail.Render branches: SaveUtil.TryGetSaveFile -> SaveCard, else the
  representative bundled sprite via FileSprite (unchanged for entities/gifts).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a `com.apple.quicklook.thumbnail` app extension (`PkmdsQuickLookThumbnail`)
alongside the existing preview extension, completing the macOS side of #935.

**What's new:**

- `Exports.cs` — `pkmds_get_sprite_path` export wraps `FileSprite.GetRelativeSpritePath`
  so Swift can ask for a sprite path without going through HTML rendering. Also adds
  `playedHours`/`playedMinutes` to the save JSON for the trainer-card layout.

- `ThumbnailProvider.swift` — `QLThumbnailProvider` that mirrors the Windows `Thumbnail.cs`
  + `SaveCard.cs` split:
  - **Saves** → compact trainer-card (version code in game accent colour, OT, TID/SID,
    playtime), drawn with AppKit (`NSBezierPath`, `NSAttributedString`) into the
    `QLThumbnailReply(contextSize:drawing:)` CGContext.
  - **Entities + wonder cards** → bundled species/item sprite scaled to the requested size.
  - **RB/GS ambiguous groups** → per-character colour interpolation on the version code
    (`"R"` red, `"B"` blue; `"G"` gold, `"S"` silver). Border remains single-colour
    (gradient border is noted as a cosmetic enhancement in the README).

- `project.yml` — new `PkmdsQuickLookThumbnail` target (type: app-extension,
  extension point `com.apple.quicklook.thumbnail`), embedded in `PkmdsHost`.

- `build-extension.sh` — after `xcodebuild`, copies the three sprite categories
  (`a/`, `ai/`, `bi/`) from `Pkmds.Rcl/wwwroot/sprites/` into the thumbnail appex
  `Contents/Resources/sprites/`, re-signs both extensions, and registers the thumbnail
  extension with `pluginkit`. Smoke tests both preview and thumbnail via `qlmanage`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@codemonkey85
Copy link
Copy Markdown
Owner Author

macOS thumbnail provider — done ✅

Just pushed the macOS QLThumbnailProvider to this branch (feat/windows-thumbnail-provider). Summary of what landed:

New files:

  • tools/macos-quicklook-poc/xcode/PkmdsQuickLookThumbnail/ThumbnailProvider.swiftQLThumbnailProvider that mirrors Thumbnail.cs + SaveCard.cs:
    • Saves → trainer card drawn with AppKit (NSBezierPath, NSAttributedString) into the QLThumbnailReply(contextSize:drawing:) CGContext. Same layout as Windows: version code (large, game-accent colour), OT, TID/SID, playtime.
    • Entities / wonder cards → bundled sprite scaled and centred, loaded from Bundle.main.resourceURL/sprites/.
    • RB/GS ambiguous groups → per-character colour interpolation on the version code (same logic as Windows gradient). Border is single-colour (gradient border is noted in the README as a cosmetic TODO).
  • ThumbnailProvider.entitlements — sandbox + cs.disable-library-validation (same as preview extension, minus network.client).
  • Info.plist — extension point com.apple.quicklook.thumbnail, same three UTIs as the preview extension.

Updated files:

  • Exports.cs — new pkmds_get_sprite_path export wraps FileSprite.GetRelativeSpritePath. Also adds playedHours/playedMinutes to the save JSON.
  • project.ymlPkmdsQuickLookThumbnail target + embed in PkmdsHost.
  • build-extension.sh — copies sprites from Pkmds.Rcl/wwwroot/sprites/{a,ai,bi}/ into the thumbnail appex after xcodebuild (the copy invalidates xcodebuild's signature, so the script re-signs both extensions). Registers the thumbnail extension with pluginkit. Smoke-tests both with qlmanage -p and qlmanage -t -s 256.

To verify locally (on a Mac with the preview extension already working):

cd tools/macos-quicklook-poc
./build-extension.sh
# Then:
qlmanage -t -s 256 -o /tmp ../../TestFiles/Lucario_B06DDFAD.pk5
qlmanage -t -s 256 -o /tmp ../../TestFiles/Test-Save-Scarlet.sav

Known limitations (noted in README):

  • Each extension embeds its own dylib copy (~34 MB total). Production would share one from the host app's Contents/Frameworks/.
  • Trainer-card border is single-colour for RB/GS — the gradient border needs CoreGraphics clip-to-stroke.
  • Sprites are copied post-build by the shell script; an Xcode Run Script phase would be cleaner.

ThumbnailsAgent refuses to register a thumbnail extension for any UTI
that declares a prohibited filename extension (.sav, .dat, .fla). Because
com.bondcodes.pkmds.save-file previously included these extensions, the
extension was silently rejected at startup and no thumbnails appeared for
.gci/.dsv/.srm files.

Fix: remove .sav, .dat, and .fla from the save-file UTI declaration in
project.yml, keeping only .gci, .dsv, and .srm. (.sav files require a
Spotlight MDImporter to assign the save-file UTI; that is future work.)

Also update build-extension.sh to:
- Proactively unregister Xcode DerivedData and iOS POC builds from the
  LS database before installing, preventing stale prohibited-extension
  registrations from resurfacing after a clean build.
- Kill ThumbnailsAgent after deploy so it restarts with the clean LS
  database (it caches extension-to-UTI mappings at startup).
- Add timeout to qlmanage -p so the smoke test doesn't block forever.
- Use a .gci fixture for the save thumbnail smoke test, since qlmanage
  cannot dispatch to sandboxed extensions via prohibited extensions.

Adds TestFiles/Test-Save-Moon.gci (copy of moon.sav renamed .gci) as
the non-prohibited save fixture for smoke tests and manual verification.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
codemonkey85 and others added 3 commits May 26, 2026 19:17
- opaqueSourceRect(): scans sprite pixels via a CG-owned bitmap context
  (data:nil / byteOrder32Little / premultipliedFirst) to find the tight
  non-transparent bounding box, then returns it in NSImage (bottom-left)
  coordinates for use as the `from:` source rect in NSImage.draw()
- Sprite load and opaque-rect computation are now done on our controlled
  background queue before the QLThumbnailReply drawing closure, avoiding
  a deadlock with AppKit's image-cache lock on the system-managed closure
  thread
- drawSprite() is removed; its logic is inlined directly into provideThumbnail
- Sprites now fill 90% of the thumbnail canvas based on visible content only
- build-extension.sh: fix `timeout` (GNU coreutils, not on macOS) for
  both qlmanage -p and qlmanage -t smoke-test calls using a background-kill
  workaround; add 15s cap to qlmanage -t to prevent build from blocking

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
QLThumbnailReply's CGContext is pixel-sized (2× point size on Retina),
but layout was using request.maximumSize in points, causing sprites to
render at ~45% fill instead of 90%.  Use ctx.width/height for all
layout inside the drawing closure.

opaqueSourceRect was also computing the NSImage y origin as minRow*sy,
treating the raster-top buffer row as NSImage bottom-left.  The correct
formula is (ph - maxRow - 1)*sy.

Also removes leftover debug logging and PNG/text file writes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@codemonkey85
Copy link
Copy Markdown
Owner Author

Superseded by #940, which contains all commits from this branch plus the subsequent macOS QL fixes (.sav/.dat/.fla split UTI, sprite/thumbnail improvements). Closing to keep the queue tidy.

@codemonkey85 codemonkey85 deleted the feat/windows-thumbnail-provider branch June 1, 2026 16:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant